//
//  BootstrapService.swift
//  SportsTime
//
//  Bootstraps canonical data from bundled JSON files into SwiftData.
//  Runs once on first launch, then relies on CloudKit for updates.
//

import Foundation
import SwiftData
import CryptoKit

actor BootstrapService {

    // MARK: - Errors

    enum BootstrapError: Error, LocalizedError {
        case bundledResourceNotFound(String)
        case jsonDecodingFailed(String, Error)
        case saveFailed(Error)

        var errorDescription: String? {
            switch self {
            case .bundledResourceNotFound(let resource):
                return "Bundled resource not found: \(resource)"
            case .jsonDecodingFailed(let resource, let error):
                return "Failed to decode \(resource): \(error.localizedDescription)"
            case .saveFailed(let error):
                return "Failed to save bootstrap data: \(error.localizedDescription)"
            }
        }
    }

    // MARK: - JSON Models (match bundled JSON structure)

    private struct JSONCanonicalStadium: Codable {
        let canonical_id: String
        let name: String
        let city: String
        let state: String?
        let latitude: Double
        let longitude: Double
        let capacity: Int
        let sport: String
        let primary_team_abbrevs: [String]
        let year_opened: Int?
        let timezone_identifier: String?
        let image_url: String?
    }

    private struct JSONCanonicalTeam: Codable {
        let canonical_id: String
        let name: String
        let abbreviation: String
        let sport: String
        let city: String
        let stadium_canonical_id: String
        let conference_id: String?
        let division_id: String?
        let primary_color: String?
        let secondary_color: String?
    }

    private struct JSONCanonicalGame: Codable {
        let canonical_id: String
        let sport: String
        let season: String
        let game_datetime_utc: String?  // ISO 8601 format
        let date: String?               // Fallback date+time format
        let time: String?               // Fallback date+time format
        let home_team_canonical_id: String
        let away_team_canonical_id: String
        let stadium_canonical_id: String?
        let is_playoff: Bool
        let broadcast_info: String?
    }

    private struct JSONStadiumAlias: Codable {
        let alias_name: String
        let stadium_canonical_id: String
        let valid_from: String?
        let valid_until: String?
    }

    private struct JSONLeagueStructure: Codable {
        let id: String
        let sport: String
        let type: String  // "conference", "division", "league"
        let name: String
        let abbreviation: String?
        let parent_id: String?
        let display_order: Int
    }

    private struct JSONTeamAlias: Codable {
        let id: String
        let team_canonical_id: String
        let alias_type: String  // "abbreviation", "name", "city"
        let alias_value: String
        let valid_from: String?
        let valid_until: String?
    }

    private struct JSONCanonicalSport: Codable {
        let sport_id: String
        let abbreviation: String
        let display_name: String
        let icon_name: String
        let color_hex: String
        let season_start_month: Int
        let season_end_month: Int
        let is_active: Bool
    }

    // MARK: - Public Methods

    /// Bootstrap canonical data from bundled JSON if not already done,
    /// or re-bootstrap if the bundled data schema version has been bumped.
    /// This is the main entry point called at app launch.
    @MainActor
    func bootstrapIfNeeded(context: ModelContext) async throws {
        let syncState = SyncState.current(in: context)
        let hasCoreCanonicalData = hasRequiredCanonicalData(context: context)

        // Re-bootstrap if bundled data version is newer (e.g., updated game schedules)
        let needsRebootstrap = syncState.bootstrapCompleted && syncState.bundledSchemaVersion < SchemaVersion.current
        if needsRebootstrap {
            syncState.bootstrapCompleted = false
        }

        // Recover from corrupted/partial local stores where bootstrap flag is true but core tables are empty.
        if syncState.bootstrapCompleted && !hasCoreCanonicalData {
            syncState.bootstrapCompleted = false
        }

        // Skip if already bootstrapped with current schema
        guard !syncState.bootstrapCompleted else {
            return
        }

        // Fresh bootstrap should always force a full CloudKit sync baseline.
        resetSyncProgress(syncState)

        // Clear any partial bootstrap data from a previous failed attempt
        try clearCanonicalData(context: context)

        // Bootstrap in dependency order:
        // 1. Stadiums (no dependencies)
        // 2. Stadium aliases (depends on stadiums)
        // 3. League structure (no dependencies)
        // 4. Teams (depends on stadiums)
        // 5. Team aliases (depends on teams)
        // 6. Games (depends on teams + stadiums)

        try await bootstrapStadiums(context: context)
        try await bootstrapStadiumAliases(context: context)
        try await bootstrapLeagueStructure(context: context)
        try await bootstrapTeams(context: context)
        try await bootstrapTeamAliases(context: context)
        try await bootstrapGames(context: context)
        try await bootstrapSports(context: context)

        // Mark bootstrap complete
        syncState.bootstrapCompleted = true
        syncState.bundledSchemaVersion = SchemaVersion.current
        syncState.lastBootstrap = Date()

        do {
            try context.save()
        } catch {
            throw BootstrapError.saveFailed(error)
        }
    }

    @MainActor
    private func resetSyncProgress(_ syncState: SyncState) {
        syncState.lastSuccessfulSync = nil
        syncState.lastSyncAttempt = nil
        syncState.lastSyncError = nil
        syncState.syncInProgress = false
        syncState.syncEnabled = true
        syncState.syncPausedReason = nil
        syncState.consecutiveFailures = 0

        syncState.stadiumChangeToken = nil
        syncState.teamChangeToken = nil
        syncState.gameChangeToken = nil
        syncState.leagueChangeToken = nil

        syncState.lastStadiumSync = nil
        syncState.lastTeamSync = nil
        syncState.lastGameSync = nil
        syncState.lastLeagueStructureSync = nil
        syncState.lastTeamAliasSync = nil
        syncState.lastStadiumAliasSync = nil
        syncState.lastSportSync = nil
    }

    // MARK: - Bootstrap Steps

    @MainActor
    private func bootstrapStadiums(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") else {
            throw BootstrapError.bundledResourceNotFound("stadiums_canonical.json")
        }

        let data: Data
        let stadiums: [JSONCanonicalStadium]

        do {
            data = try Data(contentsOf: url)
            stadiums = try JSONDecoder().decode([JSONCanonicalStadium].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("stadiums_canonical.json", error)
        }

        for jsonStadium in stadiums {
            let canonical = CanonicalStadium(
                canonicalId: jsonStadium.canonical_id,
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.stadiums,
                source: .bundled,
                name: jsonStadium.name,
                city: jsonStadium.city,
                state: (jsonStadium.state?.isEmpty ?? true) ? stateFromCity(jsonStadium.city) : jsonStadium.state!,
                latitude: jsonStadium.latitude,
                longitude: jsonStadium.longitude,
                capacity: jsonStadium.capacity,
                yearOpened: jsonStadium.year_opened,
                imageURL: jsonStadium.image_url,
                sport: jsonStadium.sport.uppercased(),
                timezoneIdentifier: jsonStadium.timezone_identifier
            )
            context.insert(canonical)
        }
    }

    @MainActor
    private func bootstrapStadiumAliases(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "stadium_aliases", withExtension: "json") else {
            return
        }

        let data: Data
        let aliases: [JSONStadiumAlias]

        do {
            data = try Data(contentsOf: url)
            aliases = try JSONDecoder().decode([JSONStadiumAlias].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("stadium_aliases.json", error)
        }

        // Build stadium lookup
        let stadiumDescriptor = FetchDescriptor<CanonicalStadium>()
        let stadiums = (try? context.fetch(stadiumDescriptor)) ?? []
        let stadiumsByCanonicalId = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.canonicalId, $0) })

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        for jsonAlias in aliases {
            let alias = StadiumAlias(
                aliasName: jsonAlias.alias_name,
                stadiumCanonicalId: jsonAlias.stadium_canonical_id,
                validFrom: jsonAlias.valid_from.flatMap { dateFormatter.date(from: $0) },
                validUntil: jsonAlias.valid_until.flatMap { dateFormatter.date(from: $0) },
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.stadiums
            )

            // Link to stadium if found
            if let stadium = stadiumsByCanonicalId[jsonAlias.stadium_canonical_id] {
                alias.stadium = stadium
            }

            context.insert(alias)
        }
    }

    @MainActor
    private func bootstrapLeagueStructure(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "league_structure", withExtension: "json") else {
            return
        }

        let data: Data
        let structures: [JSONLeagueStructure]

        do {
            data = try Data(contentsOf: url)
            structures = try JSONDecoder().decode([JSONLeagueStructure].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("league_structure.json", error)
        }

        for structure in structures {
            let structureType: LeagueStructureType
            switch structure.type.lowercased() {
            case "conference": structureType = .conference
            case "division": structureType = .division
            case "league": structureType = .league
            default: structureType = .division
            }

            let model = LeagueStructureModel(
                id: structure.id,
                sport: structure.sport.uppercased(),
                structureType: structureType,
                name: structure.name,
                abbreviation: structure.abbreviation,
                parentId: structure.parent_id,
                displayOrder: structure.display_order,
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.leagueStructure
            )
            context.insert(model)
        }
    }

    @MainActor
    private func bootstrapTeams(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") else {
            throw BootstrapError.bundledResourceNotFound("teams_canonical.json")
        }

        let data: Data
        let teams: [JSONCanonicalTeam]

        do {
            data = try Data(contentsOf: url)
            teams = try JSONDecoder().decode([JSONCanonicalTeam].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("teams_canonical.json", error)
        }

        for jsonTeam in teams {
            let team = CanonicalTeam(
                canonicalId: jsonTeam.canonical_id,
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.games,
                source: .bundled,
                name: jsonTeam.name,
                abbreviation: jsonTeam.abbreviation,
                sport: jsonTeam.sport.uppercased(),
                city: jsonTeam.city,
                stadiumCanonicalId: jsonTeam.stadium_canonical_id,
                conferenceId: jsonTeam.conference_id,
                divisionId: jsonTeam.division_id
            )
            context.insert(team)
        }
    }

    @MainActor
    private func bootstrapTeamAliases(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "team_aliases", withExtension: "json") else {
            return
        }

        let data: Data
        let aliases: [JSONTeamAlias]

        do {
            data = try Data(contentsOf: url)
            aliases = try JSONDecoder().decode([JSONTeamAlias].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("team_aliases.json", error)
        }

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        for jsonAlias in aliases {
            let aliasType: TeamAliasType
            switch jsonAlias.alias_type.lowercased() {
            case "abbreviation": aliasType = .abbreviation
            case "name": aliasType = .name
            case "city": aliasType = .city
            default: aliasType = .name
            }

            let alias = TeamAlias(
                id: jsonAlias.id,
                teamCanonicalId: jsonAlias.team_canonical_id,
                aliasType: aliasType,
                aliasValue: jsonAlias.alias_value,
                validFrom: jsonAlias.valid_from.flatMap { dateFormatter.date(from: $0) },
                validUntil: jsonAlias.valid_until.flatMap { dateFormatter.date(from: $0) },
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.games
            )
            context.insert(alias)
        }
    }

    @MainActor
    private func bootstrapGames(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "games_canonical", withExtension: "json") else {
            throw BootstrapError.bundledResourceNotFound("games_canonical.json")
        }

        let data: Data
        let games: [JSONCanonicalGame]

        do {
            data = try Data(contentsOf: url)
            games = try JSONDecoder().decode([JSONCanonicalGame].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("games_canonical.json", error)
        }

        var seenGameIds = Set<String>()
        let teams = (try? context.fetch(FetchDescriptor<CanonicalTeam>())) ?? []
        let stadiumByTeamId = Dictionary(uniqueKeysWithValues: teams.map { ($0.canonicalId, $0.stadiumCanonicalId) })

        for jsonGame in games {
            // Deduplicate
            guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
            seenGameIds.insert(jsonGame.canonical_id)

            // Parse datetime: prefer ISO 8601 format, fall back to date+time
            let dateTime: Date?
            if let iso8601String = jsonGame.game_datetime_utc {
                dateTime = parseISO8601(iso8601String)
            } else if let date = jsonGame.date {
                dateTime = parseDateTime(date: date, time: jsonGame.time ?? "7:00p")
            } else {
                dateTime = nil
            }

            guard let dateTime else { continue }

            let explicitStadium = jsonGame.stadium_canonical_id?
                .trimmingCharacters(in: .whitespacesAndNewlines)
            let resolvedStadiumCanonicalId: String
            if let explicitStadium, !explicitStadium.isEmpty {
                resolvedStadiumCanonicalId = explicitStadium
            } else if let homeStadium = stadiumByTeamId[jsonGame.home_team_canonical_id],
                      !homeStadium.isEmpty {
                resolvedStadiumCanonicalId = homeStadium
            } else if let awayStadium = stadiumByTeamId[jsonGame.away_team_canonical_id],
                      !awayStadium.isEmpty {
                resolvedStadiumCanonicalId = awayStadium
            } else {
                resolvedStadiumCanonicalId = "stadium_placeholder_\(jsonGame.canonical_id)"
            }

            let game = CanonicalGame(
                canonicalId: jsonGame.canonical_id,
                schemaVersion: SchemaVersion.current,
                lastModified: BundledDataTimestamp.games,
                source: .bundled,
                homeTeamCanonicalId: jsonGame.home_team_canonical_id,
                awayTeamCanonicalId: jsonGame.away_team_canonical_id,
                stadiumCanonicalId: resolvedStadiumCanonicalId,
                dateTime: dateTime,
                sport: jsonGame.sport.uppercased(),
                season: jsonGame.season,
                isPlayoff: jsonGame.is_playoff,
                broadcastInfo: jsonGame.broadcast_info
            )
            context.insert(game)
        }
    }

    @MainActor
    private func bootstrapSports(context: ModelContext) async throws {
        guard let url = Bundle.main.url(forResource: "sports_canonical", withExtension: "json") else {
            return
        }

        let data: Data
        let sports: [JSONCanonicalSport]

        do {
            data = try Data(contentsOf: url)
            sports = try JSONDecoder().decode([JSONCanonicalSport].self, from: data)
        } catch {
            throw BootstrapError.jsonDecodingFailed("sports_canonical.json", error)
        }

        for jsonSport in sports {
            let sport = CanonicalSport(
                id: jsonSport.sport_id,
                abbreviation: jsonSport.abbreviation,
                displayName: jsonSport.display_name,
                iconName: jsonSport.icon_name,
                colorHex: jsonSport.color_hex,
                seasonStartMonth: jsonSport.season_start_month,
                seasonEndMonth: jsonSport.season_end_month,
                isActive: jsonSport.is_active,
                lastModified: BundledDataTimestamp.sports,
                schemaVersion: SchemaVersion.current,
                source: .bundled
            )
            context.insert(sport)
        }
    }

    // MARK: - Helpers

    @MainActor
    private func clearCanonicalData(context: ModelContext) throws {
        try context.delete(model: CanonicalStadium.self)
        try context.delete(model: StadiumAlias.self)
        try context.delete(model: LeagueStructureModel.self)
        try context.delete(model: CanonicalTeam.self)
        try context.delete(model: TeamAlias.self)
        try context.delete(model: CanonicalGame.self)
        try context.delete(model: CanonicalSport.self)
    }

    @MainActor
    private func hasRequiredCanonicalData(context: ModelContext) -> Bool {
        let stadiumCount = (try? context.fetchCount(
            FetchDescriptor<CanonicalStadium>(
                predicate: #Predicate { $0.deprecatedAt == nil }
            )
        )) ?? 0
        let teamCount = (try? context.fetchCount(
            FetchDescriptor<CanonicalTeam>(
                predicate: #Predicate { $0.deprecatedAt == nil }
            )
        )) ?? 0
        let gameCount = (try? context.fetchCount(
            FetchDescriptor<CanonicalGame>(
                predicate: #Predicate { $0.deprecatedAt == nil }
            )
        )) ?? 0
        return stadiumCount > 0 && teamCount > 0 && gameCount > 0
    }

    nonisolated private func parseISO8601(_ string: String) -> Date? {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime]
        return formatter.date(from: string)
    }

    nonisolated private func parseDateTime(date: String, time: String) -> Date? {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")

        // Parse date
        formatter.dateFormat = "yyyy-MM-dd"
        guard let dateOnly = formatter.date(from: date) else { return nil }

        // Parse time (e.g., "7:30p", "10:00p", "1:05p")
        var hour = 12
        var minute = 0

        let cleanTime = time.lowercased().replacingOccurrences(of: " ", with: "")
        let isPM = cleanTime.contains("p")
        let timeWithoutAMPM = cleanTime.replacingOccurrences(of: "p", with: "").replacingOccurrences(of: "a", with: "")

        let components = timeWithoutAMPM.split(separator: ":")
        if !components.isEmpty, let h = Int(components[0]) {
            hour = h
            if isPM && hour != 12 {
                hour += 12
            } else if !isPM && hour == 12 {
                hour = 0
            }
        }
        if components.count > 1, let m = Int(components[1]) {
            minute = m
        }

        return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: dateOnly)
    }

    nonisolated private func stateFromCity(_ city: String) -> String {
        let cityToState: [String: String] = [
            "Atlanta": "GA", "Boston": "MA", "Brooklyn": "NY", "Charlotte": "NC",
            "Chicago": "IL", "Cleveland": "OH", "Dallas": "TX", "Denver": "CO",
            "Detroit": "MI", "Houston": "TX", "Indianapolis": "IN", "Los Angeles": "CA",
            "Memphis": "TN", "Miami": "FL", "Milwaukee": "WI", "Minneapolis": "MN",
            "New Orleans": "LA", "New York": "NY", "Oklahoma City": "OK", "Orlando": "FL",
            "Philadelphia": "PA", "Phoenix": "AZ", "Portland": "OR", "Sacramento": "CA",
            "San Antonio": "TX", "San Francisco": "CA", "Seattle": "WA", "Toronto": "ON",
            "Washington": "DC", "Las Vegas": "NV", "Tampa": "FL", "Pittsburgh": "PA",
            "Baltimore": "MD", "Cincinnati": "OH", "St. Louis": "MO", "Kansas City": "MO",
            "Arlington": "TX", "Anaheim": "CA", "Oakland": "CA", "San Diego": "CA",
            "Tampa Bay": "FL", "St Petersburg": "FL", "Salt Lake City": "UT"
        ]
        return cityToState[city] ?? ""
    }
}
