import Foundation

public struct PhotoCoachingPoint: Codable, Equatable, Sendable {
    public let title: String
    public let detail: String
    public init(title: String, detail: String) { self.title = title; self.detail = detail }
}

/// Authored from the actual photos, not inferred from a joint angle or a pair number.
public struct PhotoCoachingReview: Codable, Equatable, Sendable {
    public let cornerDirection: CornerDirection
    public let provenance: String
    public let points: [PhotoCoachingPoint]
    public init(cornerDirection: CornerDirection, provenance: String, points: [PhotoCoachingPoint]) {
        self.cornerDirection = cornerDirection; self.provenance = provenance; self.points = points
    }
    public func validate() throws {
        guard cornerDirection != .unknown, !provenance.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
              (1...3).contains(points.count), Set(points.map(\.title)).count == points.count,
              points.allSatisfy({ !$0.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !$0.detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) else {
            throw PoseError.invalidInput("Photo coaching needs a direction, provenance, and one to three distinct, nonempty points.")
        }
    }
}

public enum PhotoCoachingImport {
    public static func apply(_ contexts: [PairContext], to document: ReviewDocument) throws -> ReviewDocument {
        try PairReview.validate(document)
        try LeanReview.validate(document)
        guard !contexts.isEmpty, Set(contexts.map(\.pairID)).count == contexts.count else {
            throw PoseError.invalidInput("Photo coaching import has no pairs or duplicate pairs.")
        }
        var candidate = document
        candidate.schemaVersion = 3; candidate.comparisonContexts = contexts
        try PairReview.validate(candidate)
        var updated = document; updated.schemaVersion = 3
        for incoming in contexts {
            guard let coaching = incoming.photoCoaching else { throw PoseError.invalidInput("Missing photo coaching.") }
            let pair = try PairReview.resolve(document.frames).first { $0.id == incoming.pairID }!
            var context = try document.comparisonContexts.first { $0.pairID == incoming.pairID } ?? PairContext(document: document, pair: pair)
            context.photoCoaching = coaching
            updated.comparisonContexts.removeAll { $0.pairID == context.pairID }
            updated.comparisonContexts.append(context)
        }
        updated.revision += 1
        return updated
    }
}

/// Authored assessment of an unpaired subject photo. It makes no reference-rider assumption.
public struct SinglePhotoCoaching: Codable, Equatable, Sendable {
    public let frameID: String
    public let sourceHash: String
    public let panel: PixelRect
    public let provenance: String
    public let points: [PhotoCoachingPoint]
    public init(frameID: String, sourceHash: String, panel: PixelRect, provenance: String, points: [PhotoCoachingPoint]) {
        self.frameID = frameID; self.sourceHash = sourceHash; self.panel = panel
        self.provenance = provenance; self.points = points
    }
    public static func load(from url: URL, frames: [ReviewFrame]) throws -> [String: Self] {
        let entries = try JSONDecoder().decode([Self].self, from: Data(contentsOf: url))
        let pairs = try PairReview.resolve(frames)
        var result: [String: Self] = [:]
        for entry in entries {
            guard result[entry.frameID] == nil,
                  let frame = frames.first(where: { $0.id == entry.frameID }), frame.role == "subject",
                  pairs.contains(where: { $0.subjectID == frame.id && $0.referenceID == nil }),
                  frame.sourceHash == entry.sourceHash, frame.panel == entry.panel,
                  !entry.provenance.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
                  (1...3).contains(entry.points.count), Set(entry.points.map(\.title)).count == entry.points.count,
                  entry.points.allSatisfy({ !$0.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !$0.detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) else {
                throw PoseError.invalidInput("Single-photo coaching needs matching unpaired source evidence and one to three distinct, nonempty points.")
            }
            result[entry.frameID] = entry
        }
        return result
    }
}
