import Foundation

public enum PairFeature: String, Codable, CaseIterable, Sendable {
    case leftElbow, rightElbow, leftKnee, rightKnee
    public var joints: [String] {
        switch self {
        case .leftElbow: ["left_shoulder", "left_elbow", "left_wrist"]
        case .rightElbow: ["right_shoulder", "right_elbow", "right_wrist"]
        case .leftKnee: ["left_hip", "left_knee", "left_ankle"]
        case .rightKnee: ["right_hip", "right_knee", "right_ankle"]
        }
    }
    public var label: String { joints[1].replacingOccurrences(of: "_", with: " ").capitalized }
}
public enum CornerDirection: String, Codable, CaseIterable, Sendable { case unknown, left, right }
public enum ViewComparability: String, Codable, CaseIterable, Sendable { case unreviewed, comparable, notComparable }

public struct ReviewPair: Equatable, Sendable, Identifiable {
    public let id: String
    public let subjectID: String
    public let referenceID: String?
    public var frameIDs: [String] { [subjectID] + (referenceID.map { [$0] } ?? []) }
}

public struct PairContext: Codable, Equatable, Sendable {
    public let pairID: String
    public let subjectID: String
    public let referenceID: String
    public let evidenceHash: String
    public var subjectConfirmed = false
    public var referenceConfirmed = false
    public var cornerDirection: CornerDirection = .unknown
    public var comparability: [PairFeature: ViewComparability] = [:]
    public var kneePlacement: [PairFeature: KneePlacement]?
    public var photoCoaching: PhotoCoachingReview?
    public init(document: ReviewDocument, pair: ReviewPair) throws {
        guard let referenceID = pair.referenceID else { throw PoseError.invalidInput("This photo has no reference.") }
        self.pairID = pair.id; self.subjectID = pair.subjectID; self.referenceID = referenceID
        self.evidenceHash = try PairReview.evidenceHash(document, pair: pair)
    }
}

public enum PairReview {
    public static func resolve(_ frames: [ReviewFrame]) throws -> [ReviewPair] {
        guard Set(frames.map(\.id)).count == frames.count else { throw PoseError.invalidInput("Duplicate frame identity.") }
        return try Dictionary(grouping: frames, by: \.pairID).sorted { $0.key < $1.key }.map { id, members in
            let subjects = members.filter { $0.role == "subject" }
            let references = members.filter { $0.role == "reference" }
            guard !id.isEmpty, subjects.count == 1, references.count <= 1,
                  members.count == subjects.count + references.count else {
                throw PoseError.invalidInput("Invalid pair roles for \(id).")
            }
            return ReviewPair(id: id, subjectID: subjects[0].id, referenceID: references.first?.id)
        }
    }
    public static func evidenceHash(_ document: ReviewDocument, pair: ReviewPair) throws -> String {
        // Bind approvals to the source, geometry, and immutable inference snapshot, never revision or edits.
        struct Binding: Encodable {
            let manifest: String; let report: String; let frames: [Source]
        }
        struct Source: Encodable {
            let id: String; let pair: String; let role: String; let rider: String
            let file: String; let hash: String; let panel: PixelRect
        }
        let sources = try pair.frameIDs.map { id in
            guard let f = document.frames.first(where: { $0.id == id }) else { throw PoseError.invalidInput("Missing pair frame.") }
            return Source(id: f.id, pair: f.pairID, role: f.role, rider: f.rider, file: f.file, hash: f.sourceHash, panel: f.panel)
        }
        let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys]
        return try ReviewStore.digest(encoder.encode(Binding(manifest: document.manifestHash, report: document.reportHash, frames: sources)))
    }
    public static func validate(_ document: ReviewDocument) throws {
        let pairs = try resolve(document.frames)
        guard Set(document.comparisonContexts.map(\.pairID)).count == document.comparisonContexts.count,
              document.schemaVersion != 1 || document.comparisonContexts.isEmpty else { throw PoseError.invalidInput("Invalid comparison context records.") }
        for context in document.comparisonContexts {
            try context.photoCoaching?.validate()
            guard let pair = pairs.first(where: { $0.id == context.pairID }),
                  context.subjectID == pair.subjectID, context.referenceID == pair.referenceID,
                  try context.evidenceHash == evidenceHash(document, pair: pair) else {
                throw PoseError.invalidInput("Comparison approval belongs to different source evidence.")
            }
        }
    }
}

public struct ReviewEditTarget: Equatable, Sendable {
    public let session: UUID
    public let canvas: UUID
    public let frameID: String
    public let joint: String
    public init(session: UUID, canvas: UUID, frameID: String, joint: String) {
        self.session = session; self.canvas = canvas; self.frameID = frameID; self.joint = joint
    }
    public func index(in document: ReviewDocument, session: UUID, canvas: UUID, visibleIDs: [String]) -> Int? {
        guard self.session == session, self.canvas == canvas, visibleIDs.contains(frameID), InspectionRenderer.jointID(joint) != nil else { return nil }
        return document.frames.firstIndex { $0.id == frameID }
    }
}
