import Foundation

public struct LeanMeasurement: Equatable, Sendable {
    public let angle: Double
    public let lower: Double
    public let upper: Double
}

public struct LeanComparison: Sendable {
    public let measurement: LeanMeasurement?
    public let summary: String
}

public enum LeanAnalysis {
    public static let limitations = "Image-plane wheel proxy only. Annotation sensitivity excludes unknown perspective and steering error. Physical lean and causal body-position explanations are unavailable."
    /// Clockwise from image up, modulo 180 degrees. Endpoint order is immaterial.
    public static func orientation(_ line: LeanLine) -> Double {
        wrap(atan2(line.b.x - line.a.x, line.a.y - line.b.y) * 180 / .pi)
    }
    public static func wrap(_ angle: Double) -> Double {
        let a = (angle + 90).truncatingRemainder(dividingBy: 180)
        return (a < 0 ? a + 180 : a) - 90
    }
    private static func sensitivity(_ line: LeanLine) -> Double {
        asin(2 * line.uncertainty / hypot(line.b.x - line.a.x, line.b.y - line.a.y)) * 180 / .pi
    }
    /// A supplied reference is an assumed level line, whose perpendicular is upright.
    public static func magnitude(_ wheel: LeanLine, reference: LeanLine?, panel: PixelRect) -> LeanMeasurement? {
        guard wheel.valid(in: panel), reference.map({ $0.valid(in: panel) }) ?? true else { return nil }
        let upright = reference.map { orientation($0) + 90 } ?? 0
        let value = abs(wrap(orientation(wheel) - upright))
        let error = sensitivity(wheel) + (reference.map(sensitivity) ?? 0)
        return LeanMeasurement(angle: value, lower: max(0, value - error), upper: min(90, value + error))
    }
    public static func relative(_ wheel: LeanLine, reference: LeanLine, panel: PixelRect) -> LeanMeasurement? {
        guard wheel.valid(in: panel), reference.valid(in: panel) else { return nil }
        let value = wrap(orientation(wheel) - orientation(reference))
        let error = sensitivity(wheel) + sensitivity(reference)
        guard abs(value) + error < 90 else { return nil }
        return LeanMeasurement(angle: value, lower: value - error, upper: value + error)
    }
    public static func measure(_ annotation: LeanAnnotation) -> LeanMeasurement? {
        guard annotation.state != .unavailable, let wheel = annotation.wheel else { return nil }
        switch annotation.basis {
        case .imageVertical: return magnitude(wheel, reference: nil, panel: annotation.panel)
        case .assumedLevel:
            guard let reference = annotation.reference else { return nil }
            return magnitude(wheel, reference: reference, panel: annotation.panel)
        case .sharedFeature:
            guard let reference = annotation.reference else { return nil }
            return relative(wheel, reference: reference, panel: annotation.panel)
        }
    }
    public static func evidenceDigest(_ document: ReviewDocument, pair: ReviewPair) throws -> String {
        struct Evidence: Encodable { let source: String; let annotations: [LeanAnnotation] }
        let annotations = try pair.frameIDs.map { id in
            guard let a = document.leanAnnotations[id] else { throw PoseError.invalidInput("Missing lean annotation.") }
            return a
        }
        let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys]
        return try ReviewStore.digest(encoder.encode(Evidence(source: PairReview.evidenceHash(document, pair: pair), annotations: annotations)))
    }
    public static func compare(_ document: ReviewDocument, pair: ReviewPair) throws -> LeanComparison {
        func unavailable(_ text: String) -> LeanComparison { LeanComparison(measurement: nil, summary: text) }
        guard let refID = pair.referenceID, let a = document.leanAnnotations[pair.subjectID], let b = document.leanAnnotations[refID],
              a.state == .reviewed, b.state == .reviewed, let x = measure(a), let y = measure(b) else {
            return unavailable("Review both wheel axes and their reference evidence before comparing.")
        }
        try LeanReview.validate(document)
        guard a.basis == b.basis, a.cue == b.cue,
              a.basis != .sharedFeature || (!a.featureID.isEmpty && a.featureID == b.featureID) else {
            return unavailable("The two photos need compatible cues and reference bases; shared features must identify the same stationary segment.")
        }
        guard let context = document.leanContexts[pair.id], context.checksComplete,
              a.basis != .imageVertical || context.comparableCameraRoll,
              try context.approvalDigest == evidenceDigest(document, pair: pair) else {
            return unavailable("Comparison unreviewed: confirm cue, view, wheel heading, and reference comparability for this evidence.")
        }
        let measurement: LeanMeasurement
        if a.basis == .sharedFeature {
            let delta = wrap(x.angle - y.angle)
            let error = (x.upper - x.lower + y.upper - y.lower) / 2
            guard abs(delta) + error < 90 else { return unavailable("Relative orientation wraps ambiguously; ordering unavailable.") }
            measurement = LeanMeasurement(angle: delta, lower: delta-error, upper: delta+error)
        } else {
            measurement = LeanMeasurement(angle: x.angle-y.angle, lower: x.lower-y.upper, upper: x.upper-y.lower)
        }
        let summary: String
        if measurement.lower <= 0 && measurement.upper >= 0 {
            summary = "Ordering unresolved: annotation sensitivity overlaps zero."
        } else if a.basis == .sharedFeature {
            summary = "Rider rotates toward image \(measurement.angle > 0 ? "right" : "left") relative to Reference rider and the matched feature."
        } else {
            summary = "Rider shows a \(measurement.angle > 0 ? "larger" : "smaller") apparent 2D wheel angle than Reference rider."
        }
        return LeanComparison(measurement: measurement, summary: summary)
    }
}
