"""Derive a filter rim circle/axis from direct points; never infer its seating plane."""
import json
from pathlib import Path

from intake_survey import cross, dot, length, note, number, subtract


def point(value, label):
    if not isinstance(value, list) or len(value) != 3:
        raise ValueError(f'{label} needs an X/Y/Z triple.')
    return [number(v,-3000,3000,label) for v in value]


def analyze(path, comparison_limit):
    source=Path(path)
    if source.stat().st_size>100_000:
        raise ValueError('Survey exceeds 100 KB.')
    data=json.loads(source.read_text())
    json.dumps(data,allow_nan=False)  # Reject nonfinite values in opaque evidence too.
    if not isinstance(data,dict) or data.get('schema')!='midget-filter-rim-axis-survey-v1' or data.get('units')!='mm':
        raise ValueError('Use the millimeter filter rim-axis survey template.')
    if data.get('method')!='direct':
        raise ValueError('Direct spatial readings are required; a photo estimate cannot establish this axis.')
    basis=note(data.get('basis'),'Survey source')
    marking=note(data.get('filter_marking_observation'),'Actual marking observation, including unreadable')
    raw_frame=data.get('frame')
    if not isinstance(raw_frame,dict) or raw_frame.get('right_handed_orthonormal') is not True:
        raise ValueError('Confirm a right-handed orthonormal physical frame.')
    frame={key:note(raw_frame.get(key),'Frame '+key) for key in
           ('name','origin_datum','x_positive','y_positive','z_positive','configuration')}
    frame['right_handed_orthonormal']=True
    survey=data.get('survey')
    if not isinstance(survey,dict) or survey.get('same_circular_rim_section_confirmed') is not True or survey.get('closed_end_point_on_extreme_plane_normal_to_axis_confirmed') is not True:
        raise ValueError('Confirm one circular rim section and a closed-end point on its extreme plane normal to the axis. Otherwise survey the actual surface first.')
    rim_note=note(survey.get('rim_section_datum_note'),'Reproducible rim section')
    end_note=note(survey.get('closed_end_plane_datum_note'),'Closed-end extreme-plane datum')
    raw=survey.get('rim_points')
    if not isinstance(raw,list) or not 4<=len(raw)<=20:
        raise ValueError('Supply three ordered circle points and at least one independent check point (4–20 total).')
    points=[];names=set()
    for index,entry in enumerate(raw):
        if not isinstance(entry,dict):
            raise ValueError('Each rim point needs a name, coordinates and a physical datum note.')
        name=note(entry.get('name'),'Rim point name')
        if name in names:
            raise ValueError('Rim point names must be distinct.')
        names.add(name)
        coordinates=point(entry.get('xyz_mm'),name)
        if any(length(subtract(coordinates,p['xyz_mm']))<5 for p in points):
            raise ValueError('Use distinct rim locations separated by at least 5 mm (tool conditioning limit).')
        points.append({'name':name,'xyz_mm':coordinates,
                       'datum_note':note(entry.get('datum_note'),name+' datum'),
                       'role':'circle_construction' if index<3 else 'independent_check'})
    a,b,c=[p['xyz_mm'] for p in points[:3]]
    # Conditioning guards only, not survey precision or component tolerances.
    for origin,first,second in ((a,b,c),(b,a,c),(c,a,b)):
        u,v=subtract(first,origin),subtract(second,origin)
        if min(length(u),length(v))<5 or length(cross(u,v))/(length(u)*length(v))<0.08715574274765817:
            raise ValueError('Circle triangle needs sides >=5 mm and angles 5–175 degrees (tool conditioning limits).')
    u,v=subtract(b,a),subtract(c,a)
    w=cross(u,v);denominator=2*dot(w,w)
    first,second=cross(v,w),cross(w,u)
    center=[a[i]+(first[i]*dot(u,u)+second[i]*dot(v,v))/denominator for i in range(3)]
    radius=length(subtract(a,center))
    number(radius,10,100,'Derived rim radius (tool envelope range)')
    normal=[x/length(w) for x in w]
    end=point(survey.get('closed_end_plane_point_xyz_mm'),'Closed-end extreme-plane point')
    signed_depth=dot(subtract(end,center),normal)
    number(abs(signed_depth),5,250,'Derived rim-plane to closed-end distance (tool range)')
    axis=[x*(1 if signed_depth>0 else -1) for x in normal]
    depth=abs(signed_depth)
    end_center=[center[i]+depth*axis[i] for i in range(3)]
    for p in points:
        delta=subtract(p['xyz_mm'],center)
        plane=dot(delta,axis)
        radial=length(subtract(delta,[plane*x for x in axis]))
        p.update(signed_plane_residual_mm=plane,signed_radial_residual_mm=radial-radius)
    dims=data.get('independent_dimensions_mm')
    if not isinstance(dims,dict):
        raise ValueError('Record independent rim diameter and rim-plane to closed-end distance.')
    diameter=number(dims.get('rim_section_outside_diameter'),20,200,'Independent rim-section diameter')
    axial=number(dims.get('rim_plane_to_closed_end_plane'),5,250,'Independent rim-plane to closed-end distance')
    dimension_note=note(dims.get('measurement_point_and_method_note'),'Separate diameter/depth measurement points and method')
    limit=number(comparison_limit,0,10,'Explicit comparison limit, not fitted tolerance')
    diameter_residual=2*radius-diameter;depth_residual=depth-axial
    residuals=[diameter_residual,depth_residual,*[p[key] for p in points for key in
               ('signed_plane_residual_mm','signed_radial_residual_mm')]]
    maximum=max(abs(v) for v in residuals)
    return {
        'schema':'midget-filter-rim-axis-report-v1','units':'mm',
        'status':'within_entered_comparison_limit' if maximum<=limit+1e-9 else 'outside_entered_comparison_limit',
        'source_file':source.name,'method':'direct','source_note':basis,'frame':frame,
        'filter_marking_observation':marking,'rim_section_datum_note':rim_note,'closed_end_plane_datum_note':end_note,
        'derived_rim_center_xyz_mm':center,'derived_rim_radius_mm':radius,
        'derived_axis_toward_closed_end_unit_vector':axis,
        'derived_rim_to_closed_end_distance_mm':depth,
        'derived_closed_end_plane_axis_intersection_xyz_mm':end_center,
        'closed_end_plane_point_xyz_mm':end,'rim_points':points,
        'independent_dimensions_mm':{'rim_section_outside_diameter':diameter,'rim_plane_to_closed_end_plane':axial},
        'independent_dimensions_source_note':dimension_note,
        'signed_diameter_minus_independent_mm':diameter_residual,'signed_depth_minus_independent_mm':depth_residual,
        'maximum_absolute_comparison_residual_mm':maximum,'entered_comparison_limit_mm':limit,
        'original_record':data,
        'algorithm':'First three ordered points define an exact circumcircle. Remaining points independently check its plane and radius. The confirmed closed-end extreme plane chooses axis direction and axial extent. No least-squares fit, coordinate adjustment, inferred uncertainty or automatic engine registration.',
        'limits':'Reference circle and axis only, not material or an outside-volume/clearance model. The rim section is not the gasket sealing plane or an overall-height datum. No seam thickness, shell/end profile, hidden thread, adapter, gasket, current fitted identity, contact, hand/tool clearance or removal path is inferred. Frame/datum declarations are not independently verified. The entered comparison limit is not a factory or fitted tolerance.',
    }
