import Foundation
import UIKit
import ImageIO

/// Portable boundary for assistant/model output. Never trusts a model's review flags or file paths.
struct ChartPackage: Codable {
    struct Block: Codable {
        var name: String
        var firstPrintedRow: Int
        var rows: [String]
        var sourceImageBase64: String?
        var sourceRowCorners: [[NormalizedPoint]]?
        var correctedImageBase64: String?
        var techniqueCells: [[Cell]]? = nil
        var rowPlans: [ChartRowPlan]? = nil
        var sourceColumns: Int? = nil
        var sourceColumnMap: [Int?]? = nil
    }
    struct Section: Codable {
        var block: Int
        var firstRow: Int
        var lastRow: Int
        var repeats: Int
        var restartStitches: Int?
        var startNote: String?
    }
    struct Rules: Codable {
        var frontView: Bool
        var firstRowRightSide: Bool
        var firstRowRightToLeft: Bool
        var circular: Bool?
    }
    var format: String
    var version: Int
    var name: String
    var note: String
    var sourceImageBase64: String?
    var blocks: [Block]
    var sections: [Section]
    var rules: Rules
    var castOnStitches: Int? = nil
    var firstRowIsCastOn: Bool? = nil
    var finishingNote: String? = nil

    @MainActor static func export(_ project: KnittingProject, from store: ProjectStore) throws -> Data {
        let indices = Dictionary(uniqueKeysWithValues: project.blocks.enumerated().map { ($0.element.id, $0.offset) })
        guard project.sections.allSatisfy({ indices[$0.blockID] != nil }) else { throw RecognitionError.invalidGrid }
        let sharedSource = project.blocks.allSatisfy({ $0.sourceFile == project.blocks.first?.sourceFile }) ? project.blocks.first?.sourceFile : nil
        func encodedImage(_ file: String?) throws -> String? {
            guard let file else { return nil }
            guard let data = store.imageData(file) else { throw CocoaError(.fileReadNoSuchFile) }
            return data.base64EncodedString()
        }
        let package = ChartPackage(
            format: "maozhizhi.chart", version: project.hasTechniques ? 6 : project.blocks.contains { $0.cells.contains { $0.contains { $0.stitch.cableWidth == 5 } } } ? 5 : (project.columns > 120 || (project.castOnStitches ?? 0) > 120 || project.rules.circular == true || project.sections.contains { $0.restartStitches != nil || $0.startNote != nil } || project.blocks.contains { $0.cells.contains { $0.contains { $0.stitch.cableWidth != nil } } }) ? 4 : project.blocks.contains { $0.cells.contains { $0.contains { [.makeRight, .slipFront].contains($0.stitch) } } } ? 3 : (project.usesExtendedChart ? 2 : 1), name: project.name,
            note: project.importNote ?? "", sourceImageBase64: try encodedImage(sharedSource),
            blocks: try project.blocks.map { block in
                Block(name: block.name, firstPrintedRow: block.firstPrintedRow,
                      rows: block.cells.map { row in String(row.map { $0.stitch.code }) },
                      sourceImageBase64: block.sourceFile == sharedSource ? nil : try encodedImage(block.sourceFile),
                      sourceRowCorners: block.sourceRowCorners,
                      correctedImageBase64: try encodedImage(block.correctedFile),
                      techniqueCells: project.hasTechniques ? block.cells.map { $0.map { $0.unreviewed } } : nil, rowPlans: block.rowPlans, sourceColumns: block.sourceColumns, sourceColumnMap: block.sourceColumnMap)
            },
            sections: project.sections.map { Section(block: indices[$0.blockID]!, firstRow: $0.firstRow, lastRow: $0.lastRow, repeats: $0.repeats, restartStitches: $0.restartStitches, startNote: $0.startNote) },
            rules: Rules(frontView: project.rules.frontView, firstRowRightSide: project.rules.firstRowRightSide,
                         firstRowRightToLeft: project.rules.firstRowRightToLeft, circular: project.rules.circular == true ? true : nil),
            castOnStitches: project.castOnStitches, firstRowIsCastOn: project.firstRowIsCastOn,
            finishingNote: project.finishingNote)
        let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys]
        let data = try encoder.encode(package)
        _ = try decode(data)
        return data
    }

    static func withoutImages(_ project: KnittingProject) throws -> Data {
        let indices = Dictionary(uniqueKeysWithValues: project.blocks.enumerated().map { ($0.element.id, $0.offset) })
        guard project.sections.allSatisfy({ indices[$0.blockID] != nil }) else { throw RecognitionError.invalidGrid }
        let package = ChartPackage(format: "maozhizhi.chart", version: 6, name: project.name, note: project.importNote ?? "",
            blocks: project.blocks.map { Block(name: $0.name, firstPrintedRow: $0.firstPrintedRow,
                rows: $0.cells.map { String($0.map { $0.stitch.code }) },
                techniqueCells: $0.cells.map { $0.map { $0.unreviewed } }, rowPlans: $0.rowPlans, sourceColumns: $0.sourceColumns, sourceColumnMap: $0.sourceColumnMap) },
            sections: project.sections.map { Section(block: indices[$0.blockID]!, firstRow: $0.firstRow, lastRow: $0.lastRow, repeats: $0.repeats, restartStitches: $0.restartStitches, startNote: $0.startNote) },
            rules: Rules(frontView: project.rules.frontView, firstRowRightSide: project.rules.firstRowRightSide, firstRowRightToLeft: project.rules.firstRowRightToLeft, circular: project.rules.circular),
            castOnStitches: project.castOnStitches, firstRowIsCastOn: project.firstRowIsCastOn, finishingNote: project.finishingNote)
        let data = try JSONEncoder().encode(package)
        _ = try decode(data)
        return data
    }

    static func decode(_ data: Data) throws -> ChartPackage {
        guard data.count <= 32_000_000 else { throw RecognitionError.invalidGrid }
        let package = try JSONDecoder().decode(Self.self, from: data)
        guard package.format == "maozhizhi.chart", (1...6).contains(package.version),
              (1...12).contains(package.blocks.count), (1...100).contains(package.sections.count),
              !package.name.isEmpty, package.name.count <= 160, package.note.count <= 5000 else { throw RecognitionError.invalidGrid }
        guard package.castOnStitches.map({ (1...(package.version >= 6 ? 2000 : package.version >= 4 ? 240 : 120)).contains($0) }) ?? true,
              (package.finishingNote?.count ?? 0) <= 5000,
              package.version >= 2 || (package.castOnStitches == nil && package.firstRowIsCastOn == nil && package.finishingNote == nil)
        else { throw RecognitionError.invalidGrid }
        guard package.version >= 4 || (package.rules.circular == nil && package.sections.allSatisfy { $0.restartStitches == nil && $0.startNote == nil }) else { throw RecognitionError.invalidGrid }
        let alphabet = package.version == 1 ? "KPU" : package.version == 2 ? "KPU.OMNLRDE" : package.version == 3 ? "KPU.OMNLRDETS" : package.version == 4 ? "ABCFKPU.OMNLRDETS" : String(Stitch.allCases.map(\.code))
        var width: Int?
        for block in package.blocks {
            guard (1...400).contains(block.rows.count), let first = block.rows.first,
                  (1...(package.version >= 4 ? 240 : 120)).contains(first.count), (0...10000).contains(block.firstPrintedRow), block.name.count <= 160 else { throw RecognitionError.invalidGrid }
            if let width, width != first.count { throw RecognitionError.invalidGrid }
            width = first.count
            guard block.rows.allSatisfy({ $0.count == first.count && $0.allSatisfy { alphabet.contains($0) } }) else { throw RecognitionError.invalidGrid }
            guard package.version >= 6 || (block.techniqueCells == nil && block.rowPlans == nil) else { throw RecognitionError.invalidGrid }
            if let cells = block.techniqueCells {
                guard cells.count == block.rows.count, cells.allSatisfy({ $0.count == first.count && $0.allSatisfy { $0.validColor && ($0.symbol?.valid ?? true) } }),
                      cells.enumerated().allSatisfy({ r, row in String(row.map { $0.stitch.code }) == block.rows[r] }) else { throw RecognitionError.invalidGrid }
            }
            if let map = block.sourceColumnMap {
                guard package.version >= 6, let sourceColumns = block.sourceColumns, (1...240).contains(sourceColumns), map.count == first.count,
                      map.allSatisfy({ $0.map { (0..<sourceColumns).contains($0) } ?? true }) else { throw RecognitionError.invalidGrid }
            } else if block.sourceColumns != nil { throw RecognitionError.invalidGrid }
            if let plans = block.rowPlans { guard plans.count == block.rows.count, plans.allSatisfy({ $0.valid }) else { throw RecognitionError.invalidGrid } }
            if let quads = block.sourceRowCorners {
                guard quads.count == block.rows.count, quads.allSatisfy(validQuad) else { throw RecognitionError.invalidCorners }
            }
            if let encoded = block.sourceImageBase64 { _ = try imageData(encoded) }
            if let encoded = block.correctedImageBase64 { _ = try imageData(encoded) }
        }
        var total = 0
        for section in package.sections {
            guard package.blocks.indices.contains(section.block), (1...100).contains(section.repeats),
                  section.firstRow >= 1, section.lastRow >= section.firstRow,
                  section.lastRow <= package.blocks[section.block].rows.count,
                  section.restartStitches.map({ (1...(package.version >= 6 ? 2000 : 240)).contains($0) }) ?? true,
                  (section.startNote?.count ?? 0) <= 2000 else { throw RecognitionError.invalidGrid }
            total += (section.lastRow - section.firstRow + 1) * section.repeats
        }
        guard total <= 20000 else { throw RecognitionError.invalidGrid }
        if let encoded = package.sourceImageBase64 { _ = try imageData(encoded) }
        return package
    }
    private static func validQuad(_ quad: [NormalizedPoint]) -> Bool {
        guard quad.count == 4, quad.allSatisfy({ $0.x.isFinite && $0.y.isFinite && (0...1).contains($0.x) && (0...1).contains($0.y) }) else { return false }
        return (0..<4).allSatisfy { i in
            let a = quad[i], b = quad[(i+1)%4], c = quad[(i+2)%4]
            return (b.x-a.x)*(c.y-b.y) - (b.y-a.y)*(c.x-b.x) > 0.00000001
        }
    }
    private static func imageData(_ encoded: String) throws -> Data {
        guard let data = Data(base64Encoded: encoded),
              let source = CGImageSourceCreateWithData(data as CFData, nil),
              let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
              let width = properties[kCGImagePropertyPixelWidth] as? Int,
              let height = properties[kCGImagePropertyPixelHeight] as? Int,
              width > 0, height > 0, width <= 6000, height <= 16000, width * height <= 20_000_000 else { throw RecognitionError.unreadable }
        return data
    }
    @MainActor func importProject(into store: ProjectStore) throws -> KnittingProject {
        var createdFiles: [String] = []
        do {
            func saveImage(_ encoded: String?) throws -> String? {
                guard let encoded else { return nil }
                let name = try store.writeImage(Self.imageData(encoded)); createdFiles.append(name); return name
            }
            let original = try saveImage(sourceImageBase64)
            var chartBlocks: [ChartBlock] = []
            for block in blocks {
                let cells = block.techniqueCells?.map { $0.map { $0.unreviewed } } ?? block.rows.map { $0.map { Cell(Stitch(code: $0) ?? .unknown) } }
                var chart = ChartBlock(name: block.name, sourceFile: try saveImage(block.sourceImageBase64) ?? original, correctedFile: try saveImage(block.correctedImageBase64), cells: cells, firstPrintedRow: block.firstPrintedRow)
                chart.sourceRowCorners = block.sourceRowCorners
                chart.sourceColumns = block.sourceColumns
                chart.sourceColumnMap = block.sourceColumnMap
                chart.rowPlans = block.rowPlans
                if let rows = block.sourceRowCorners, let bottom = rows.first, let top = rows.last { chart.corners = [top[0],top[1],bottom[2],bottom[3]] }
                chartBlocks.append(chart)
            }
            var project = KnittingProject(name: name, blocks: chartBlocks, sections: sections.map { .init(blockID: chartBlocks[$0.block].id, firstRow: $0.firstRow, lastRow: $0.lastRow, repeats: $0.repeats, restartStitches: $0.restartStitches, startNote: $0.startNote) })
            project.rules = .init(frontView: rules.frontView, firstRowRightSide: rules.firstRowRightSide, firstRowRightToLeft: rules.firstRowRightToLeft, confirmed: false, circular: rules.circular)
            project.importNote = note
            project.castOnStitches = castOnStitches
            project.firstRowIsCastOn = firstRowIsCastOn
            project.finishingNote = finishingNote
            if version >= 2 { project.localReviewOnly = true }
            guard store.save(project) else { throw CocoaError(.fileWriteUnknown) }
            return project
        } catch {
            for file in createdFiles { try? FileManager.default.removeItem(at: store.root.appendingPathComponent(file)) }
            throw error
        }
    }
}

extension ProjectStore {
    func addYokotaIfNeeded() {
        let marker = root.appendingPathComponent("yokota-review-v5-20261024-installed")
        guard !FileManager.default.fileExists(atPath: marker.path),
              let url = Bundle.main.url(forResource: "yokota-hat.maozhizhi", withExtension: "json") else { return }
        do {
            _ = try ChartPackage.decode(Data(contentsOf: url)).importProject(into: self)
            try Data().write(to: marker, options: .atomic)
        } catch { errorMessage = L("package.failed") }
    }
    func addMaoerIfNeeded() {
        let marker = root.appendingPathComponent("maoer-review-v3-installed")
        guard !FileManager.default.fileExists(atPath: marker.path),
              let url = Bundle.main.url(forResource: "maoer-scarf.maozhizhi", withExtension: "json") else { return }
        do {
            _ = try ChartPackage.decode(Data(contentsOf: url)).importProject(into: self)
            try Data().write(to: marker, options: .atomic)
        } catch { errorMessage = L("package.failed") }
    }
    func addSophieIfNeeded() {
        let marker = root.appendingPathComponent("sophie-review-v2-installed")
        guard !FileManager.default.fileExists(atPath: marker.path),
              let url = Bundle.main.url(forResource: "sophie-scarf.maozhizhi", withExtension: "json") else { return }
        do {
            let package = try ChartPackage.decode(Data(contentsOf: url))
            _ = try package.importProject(into: self)
            try Data().write(to: marker, options: .atomic)
        } catch { errorMessage = L("package.failed") }
    }
    /// Stable identities and individual markers preserve edits, progress, and deletion on upgrade.
    enum BuiltInExample: String, CaseIterable {
        case guernsey, yokota, sophie
        var id: UUID {
            switch self {
            case .guernsey: return UUID(uuidString: "B45CF2B8-C5E8-4B0A-9B84-EAB93B156384")!
            case .yokota: return UUID(uuidString: "719AD014-C881-4843-932C-A9673EA10127")!
            case .sophie: return UUID(uuidString: "8444132D-E0B8-43C4-ACBC-F260B4720354")!
            }
        }
        var marker: String { rawValue + "-default-example-v1-installed" }
    }
    static let defaultExampleID = BuiltInExample.guernsey.id

    static func makeDefaultExample() throws -> KnittingProject {
        try makeExample(.guernsey)
    }

    static func makeExample(_ example: BuiltInExample) throws -> KnittingProject {
        guard let url = Bundle.main.url(forResource: example.rawValue + "-example", withExtension: "json") else { throw CocoaError(.fileNoSuchFile) }
        let package = try ChartPackage.decode(Data(contentsOf: url))
        // Dedicated bundled resources contain chart data only, never personal source photos.
        let blocks = package.blocks.enumerated().map { index, block in
            ChartBlock(name: LF("example.block", index + 1), cells: block.rows.map { row in
                row.map { Cell(Stitch(code: $0) ?? .unknown, reviewed: true) }
            }, firstPrintedRow: block.firstPrintedRow)
        }
        var project = KnittingProject(name: L("example." + example.rawValue), blocks: blocks,
            sections: package.sections.map { .init(blockID: blocks[$0.block].id, firstRow: $0.firstRow, lastRow: $0.lastRow, repeats: $0.repeats,
                restartStitches: $0.restartStitches, startNote: $0.startNote) })
        project.rules = .init(frontView: package.rules.frontView, firstRowRightSide: package.rules.firstRowRightSide,
                              firstRowRightToLeft: package.rules.firstRowRightToLeft, confirmed: true, circular: package.rules.circular)
        project.castOnStitches = package.castOnStitches
        project.firstRowIsCastOn = package.firstRowIsCastOn
        project.finishingNote = package.finishingNote
        project.isSample = true
        project.localReviewOnly = true
        project.confirmedRevision = project.revision
        guard project.isConfirmed else { throw RecognitionError.invalidGrid }
        return project
    }

    func addDefaultExampleIfNeeded() {
        for kind in BuiltInExample.allCases {
            let marker = root.appendingPathComponent(kind.marker)
            guard !FileManager.default.fileExists(atPath: marker.path) else { continue }
            do {
                if !projects.contains(where: { $0.id == kind.id }) {
                    var example = try Self.makeExample(kind)
                    example.id = kind.id
                    guard save(example) else { return }
                }
                try Data().write(to: marker, options: .atomic)
            } catch { errorMessage = L("package.failed") }
        }
    }

    func addReferenceIfNeeded() {
        let marker = root.appendingPathComponent("guernsey-reference-v1-installed")
        guard !FileManager.default.fileExists(atPath: marker.path),
              let url = Bundle.main.url(forResource: "guernsey-scarf.maozhizhi", withExtension: "json") else { return }
        do {
            let package = try ChartPackage.decode(Data(contentsOf: url))
            _ = try package.importProject(into: self)
            try Data().write(to: marker, options: .atomic)
        } catch { errorMessage = L("package.failed") }
    }
}

enum ChartSharing {
    @MainActor static func fileURL(for project: KnittingProject, in store: ProjectStore) throws -> URL {
        let directory = FileManager.default.temporaryDirectory.appendingPathComponent("MaoZhiZhi-Share-" + UUID().uuidString, isDirectory: true)
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
        let name = project.name.replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ":", with: "-")
        let url = directory.appendingPathComponent(String(name.prefix(80)) + ".maozhizhi")
        try ChartPackage.export(project, from: store).write(to: url, options: .atomic)
        return url
    }
}
