import Foundation

enum LevelLoadError: Error, LocalizedError {
    case fileNotFound(String)
    case decodingFailed(String, Error)
    case validationFailed(String, String)

    var errorDescription: String? {
        switch self {
        case .fileNotFound(let name): return "Level file not found: \(name)"
        case .decodingFailed(let name, let e): return "Failed to decode \(name): \(e)"
        case .validationFailed(let id, let reason): return "Level \(id) invalid: \(reason)"
        }
    }
}

struct LevelLoader {

    // MARK: - Load single level

    static func load(levelId: String, world: Int) throws -> GameState {
        // World 2 files are prefixed w2_ to avoid name collision with world 1
        let filename = world >= 2 ? "w2_\(levelId)" : levelId
        guard let url = Bundle.main.url(forResource: filename, withExtension: "json") else {
            throw LevelLoadError.fileNotFound("\(filename).json")
        }
        let data = try Data(contentsOf: url)
        let def: LevelDefinition
        do {
            def = try JSONDecoder().decode(LevelDefinition.self, from: data)
        } catch {
            throw LevelLoadError.decodingFailed(filename, error)
        }
        try validate(def)
        return makeGameState(from: def)
    }

    // MARK: - Load from definition (used in tests with in-memory JSON)

    static func makeGameState(from def: LevelDefinition) -> GameState {
        let gridSize = GridSize(width: def.grid.width, height: def.grid.height)

        let blocks = def.blocks.enumerated().map { idx, b in
            Block(
                id: idx,
                position: GridPosition(x: b.x, y: b.y),
                color: BlockColor(rawValue: b.color) ?? .red
            )
        }

        let walls: Set<GridPosition> = Set((def.walls ?? []).map {
            GridPosition(x: $0.x, y: $0.y)
        })

        let objective = parseObjective(def.objective)

        return GameState(
            grid: gridSize,
            blocks: blocks,
            walls: walls,
            moveCount: 0,
            par: def.par,
            objective: objective,
            allowsMerging: def.world >= 2,
            history: []
        )
    }

    // MARK: - Metadata

    static func loadMetadata() -> MetadataFile? {
        guard let url = Bundle.main.url(forResource: "metadata", withExtension: "json") else { return nil }
        return try? JSONDecoder().decode(MetadataFile.self, from: Data(contentsOf: url))
    }

    // MARK: - Validation

    static func validate(_ def: LevelDefinition) throws {
        let id = def.id
        guard def.grid.width >= 3, def.grid.width <= 10,
              def.grid.height >= 3, def.grid.height <= 10 else {
            throw LevelLoadError.validationFailed(id, "Grid dimensions out of range 3–10")
        }
        guard def.par > 0 else {
            throw LevelLoadError.validationFailed(id, "Par must be positive")
        }
        guard def.blocks.count >= 2 else {
            throw LevelLoadError.validationFailed(id, "At least 2 blocks required")
        }

        var occupied = Set<String>()
        func cell(_ x: Int, _ y: Int) -> String { "\(x),\(y)" }

        for b in def.blocks {
            guard b.x >= 0, b.x < def.grid.width, b.y >= 0, b.y < def.grid.height else {
                throw LevelLoadError.validationFailed(id, "Block (\(b.x),\(b.y)) out of bounds")
            }
            let key = cell(b.x, b.y)
            guard !occupied.contains(key) else {
                throw LevelLoadError.validationFailed(id, "Duplicate position (\(b.x),\(b.y))")
            }
            occupied.insert(key)
        }
        for w in def.walls ?? [] {
            guard w.x >= 0, w.x < def.grid.width, w.y >= 0, w.y < def.grid.height else {
                throw LevelLoadError.validationFailed(id, "Wall (\(w.x),\(w.y)) out of bounds")
            }
            let key = cell(w.x, w.y)
            guard !occupied.contains(key) else {
                throw LevelLoadError.validationFailed(id, "Wall overlaps object at (\(w.x),\(w.y))")
            }
            occupied.insert(key)
        }

        let validObjectives = ["clear_all", "clear_color", "reduce_to", "clear_targets"]
        guard validObjectives.contains(def.objective.type) else {
            throw LevelLoadError.validationFailed(id, "Unknown objective type: \(def.objective.type)")
        }
    }

    // MARK: - Private helpers

    private static func parseObjective(_ obj: ObjectiveDef) -> ObjectiveType {
        switch obj.type {
        case "clear_all":
            return .clearAll
        case "clear_color":
            let color = BlockColor(rawValue: obj.color ?? "red") ?? .red
            return .clearColor(color)
        case "reduce_to":
            return .reduceTo(obj.count ?? 1)
        default:
            return .clearAll
        }
    }
}

// MARK: - Metadata Codable types

struct MetadataFile: Codable {
    let version: String
    let worlds: [WorldMeta]
}

struct WorldMeta: Codable {
    let id: Int
    let name: String
    let description: String
    let newMechanic: String?
    let unlockStars: Int?
    let levels: [LevelMeta]

    enum CodingKeys: String, CodingKey {
        case id, name, description, levels
        case newMechanic = "new_mechanic"
        case unlockStars = "unlock_stars"
    }
}

struct LevelMeta: Codable {
    let id: String
    let title: String
    let file: String
    let isChallenge: Bool
    let starsRequired: Int?

    enum CodingKeys: String, CodingKey {
        case id, title, file
        case isChallenge = "is_challenge"
        case starsRequired = "stars_required"
    }
}
