import Foundation

public struct RiderInput: Codable, Sendable {
    public var role: String
    public var rider: String
    public var file: String? = nil
    public var panel: PixelRect
    public var crop: PixelRect
    public var annotations: [Landmark]
    public var torsoLength: Double?
    public var annotationNote: String
}
public struct PairInput: Codable, Sendable {
    public var id: String
    public var file: String
    public var riders: [RiderInput]
}
public struct SessionManifest: Codable, Sendable {
    public var schemaVersion: Int
    public var session: String
    public var annotationProvenance: String
    public var pairs: [PairInput]
    public static func load(url: URL) throws -> Self {
        let manifest = try JSONDecoder().decode(Self.self, from: Data(contentsOf: url))
        guard manifest.schemaVersion == 1, !manifest.pairs.isEmpty,
              Set(manifest.pairs.map(\.id)).count == manifest.pairs.count else {
            throw PoseError.invalidInput("Unsupported schema, empty manifest, or duplicate pair IDs.")
        }
        for pair in manifest.pairs {
            let roles = Set(pair.riders.map(\.role))
            let validRoles = (pair.riders.count == 1 && roles == ["subject"]) ||
                (pair.riders.count == 2 && roles == ["subject", "reference"])
            guard pair.id.range(of: "^[A-Za-z0-9_-]+$", options: .regularExpression) != nil,
                  validRoles else {
                throw PoseError.invalidInput("Each entry needs a safe ID and one subject, with at most one reference.")
            }
            _ = try manifest.assetURL(for: pair, manifestURL: url)
            for rider in pair.riders {
                _ = try manifest.assetURL(for: pair, rider: rider, manifestURL: url)
            }
        }
        return manifest
    }
    public func assetURL(for pair: PairInput, rider: RiderInput? = nil, manifestURL: URL) throws -> URL {
        let file = rider?.file ?? pair.file
        let base = manifestURL.deletingLastPathComponent().resolvingSymlinksInPath()
        guard !file.hasPrefix("/") else { throw PoseError.invalidInput("Asset path must be relative.") }
        let candidate = base.appendingPathComponent(file).resolvingSymlinksInPath()
        guard candidate.path.hasPrefix(base.path + "/") else { throw PoseError.invalidInput("Asset escapes session folder.") }
        return candidate
    }
}
