//
//  CanonicalModels.swift
//  SportsTime
//
//  SwiftData models for canonical data: stadiums, teams, games, and league structure.
//  These are the runtime source of truth, populated from bundled JSON and synced via CloudKit.
//

import Foundation
import SwiftData
import CryptoKit

// MARK: - Schema Version

/// Schema version constants for canonical data models.
/// Marked nonisolated to allow access from any isolation domain.
nonisolated enum SchemaVersion {
    static let current: Int = 2
    static let minimumSupported: Int = 1
}

// MARK: - Data Source

enum DataSource: String, Codable {
    case bundled        // Shipped with app bundle
    case cloudKit       // Synced from CloudKit
    case userCorrection // User-provided correction
}

// MARK: - Team Alias Type

enum TeamAliasType: String, Codable {
    case abbreviation  // Old abbreviation (e.g., "NJN" -> "BRK")
    case name          // Old team name (e.g., "New Jersey Nets")
    case city          // Old city (e.g., "New Jersey")
}

// MARK: - League Structure Type

enum LeagueStructureType: String, Codable {
    case conference
    case division
    case league
}

// MARK: - Sync State

@Model
final class SyncState {
    @Attribute(.unique) var id: String = "singleton"

    // Bootstrap tracking
    var bootstrapCompleted: Bool = false
    var bundledSchemaVersion: Int = 0
    var lastBootstrap: Date?

    // CloudKit sync tracking
    var lastSuccessfulSync: Date?
    var lastSyncAttempt: Date?
    var lastSyncError: String?
    var syncInProgress: Bool = false
    var syncEnabled: Bool = true
    var syncPausedReason: String?
    var consecutiveFailures: Int = 0

    // Change tokens for delta sync
    var stadiumChangeToken: Data?
    var teamChangeToken: Data?
    var gameChangeToken: Data?
    var leagueChangeToken: Data?

    // Per-entity sync timestamps for partial progress
    var lastStadiumSync: Date?
    var lastTeamSync: Date?
    var lastGameSync: Date?
    var lastLeagueStructureSync: Date?
    var lastTeamAliasSync: Date?
    var lastStadiumAliasSync: Date?
    var lastSportSync: Date?

    init() {}

    static func current(in context: ModelContext) -> SyncState {
        let descriptor = FetchDescriptor<SyncState>(
            predicate: #Predicate { $0.id == "singleton" }
        )
        if let existing = try? context.fetch(descriptor).first {
            return existing
        }
        let new = SyncState()
        context.insert(new)
        return new
    }
}

// MARK: - Canonical Stadium

@Model
final class CanonicalStadium {
    // Identity
    @Attribute(.unique) var canonicalId: String
    var uuid: UUID

    // Versioning
    var schemaVersion: Int
    var lastModified: Date
    var sourceRaw: String

    // Deprecation (soft delete)
    var deprecatedAt: Date?
    var deprecationReason: String?
    var replacedByCanonicalId: String?

    // Core data
    var name: String
    var city: String
    var state: String
    var latitude: Double
    var longitude: Double
    var capacity: Int
    var yearOpened: Int?
    var imageURL: String?
    var sport: String
    var timezoneIdentifier: String?

    // User-correctable fields (preserved during sync)
    var userNickname: String?
    var userNotes: String?
    var isFavorite: Bool = false

    // Relationships
    @Relationship(deleteRule: .cascade, inverse: \StadiumAlias.stadium)
    var aliases: [StadiumAlias]?

    init(
        canonicalId: String,
        uuid: UUID? = nil,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date(),
        source: DataSource = .bundled,
        name: String,
        city: String,
        state: String,
        latitude: Double,
        longitude: Double,
        capacity: Int,
        yearOpened: Int? = nil,
        imageURL: String? = nil,
        sport: String,
        timezoneIdentifier: String? = nil
    ) {
        self.canonicalId = canonicalId
        self.uuid = uuid ?? Self.deterministicUUID(from: canonicalId)
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
        self.sourceRaw = source.rawValue
        self.name = name
        self.city = city
        self.state = state
        self.latitude = latitude
        self.longitude = longitude
        self.capacity = capacity
        self.yearOpened = yearOpened
        self.imageURL = imageURL
        self.sport = sport
        self.timezoneIdentifier = timezoneIdentifier
    }

    var source: DataSource {
        get { DataSource(rawValue: sourceRaw) ?? .bundled }
        set { sourceRaw = newValue.rawValue }
    }

    var isActive: Bool { deprecatedAt == nil }

    func toDomain() -> Stadium {
        Stadium(
            id: canonicalId,
            name: name,
            city: city,
            state: state,
            latitude: latitude,
            longitude: longitude,
            capacity: capacity,
            sport: Sport(rawValue: sport) ?? .mlb,
            yearOpened: yearOpened,
            imageURL: imageURL.flatMap { URL(string: $0) },
            timeZoneIdentifier: timezoneIdentifier
        )
    }

    static func deterministicUUID(from string: String) -> UUID {
        let data = Data(string.utf8)
        let hash = SHA256.hash(data: data)
        let hashBytes = Array(hash)
        var bytes = Array(hashBytes.prefix(16))
        // Set version 4 and variant bits
        bytes[6] = (bytes[6] & 0x0F) | 0x40
        bytes[8] = (bytes[8] & 0x3F) | 0x80
        return UUID(uuid: (
            bytes[0], bytes[1], bytes[2], bytes[3],
            bytes[4], bytes[5], bytes[6], bytes[7],
            bytes[8], bytes[9], bytes[10], bytes[11],
            bytes[12], bytes[13], bytes[14], bytes[15]
        ))
    }
}

// MARK: - Stadium Alias

@Model
final class StadiumAlias {
    @Attribute(.unique) var aliasName: String
    var stadiumCanonicalId: String
    var validFrom: Date?
    var validUntil: Date?
    var schemaVersion: Int
    var lastModified: Date

    var stadium: CanonicalStadium?

    init(
        aliasName: String,
        stadiumCanonicalId: String,
        validFrom: Date? = nil,
        validUntil: Date? = nil,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date()
    ) {
        self.aliasName = aliasName.lowercased()
        self.stadiumCanonicalId = stadiumCanonicalId
        self.validFrom = validFrom
        self.validUntil = validUntil
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
    }
}

// MARK: - Canonical Team

@Model
final class CanonicalTeam {
    @Attribute(.unique) var canonicalId: String
    var uuid: UUID

    var schemaVersion: Int
    var lastModified: Date
    var sourceRaw: String

    var deprecatedAt: Date?
    var deprecationReason: String?
    var relocatedToCanonicalId: String?

    var name: String
    var abbreviation: String
    var sport: String
    var city: String
    var stadiumCanonicalId: String
    var logoURL: String?
    var primaryColor: String?
    var secondaryColor: String?
    var conferenceId: String?
    var divisionId: String?

    // User-correctable
    var userNickname: String?
    var isFavorite: Bool = false

    @Relationship(deleteRule: .cascade, inverse: \TeamAlias.team)
    var aliases: [TeamAlias]?

    init(
        canonicalId: String,
        uuid: UUID? = nil,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date(),
        source: DataSource = .bundled,
        name: String,
        abbreviation: String,
        sport: String,
        city: String,
        stadiumCanonicalId: String,
        logoURL: String? = nil,
        primaryColor: String? = nil,
        secondaryColor: String? = nil,
        conferenceId: String? = nil,
        divisionId: String? = nil
    ) {
        self.canonicalId = canonicalId
        self.uuid = uuid ?? CanonicalStadium.deterministicUUID(from: canonicalId)
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
        self.sourceRaw = source.rawValue
        self.name = name
        self.abbreviation = abbreviation
        self.sport = sport
        self.city = city
        self.stadiumCanonicalId = stadiumCanonicalId
        self.logoURL = logoURL
        self.primaryColor = primaryColor
        self.secondaryColor = secondaryColor
        self.conferenceId = conferenceId
        self.divisionId = divisionId
    }

    var source: DataSource {
        get { DataSource(rawValue: sourceRaw) ?? .bundled }
        set { sourceRaw = newValue.rawValue }
    }

    var isActive: Bool { deprecatedAt == nil }

    var sportEnum: Sport? { Sport(rawValue: sport) }

    func toDomain() -> Team {
        Team(
            id: canonicalId,
            name: name,
            abbreviation: abbreviation,
            sport: sportEnum ?? .mlb,
            city: city,
            stadiumId: stadiumCanonicalId,
            conferenceId: conferenceId,
            divisionId: divisionId,
            logoURL: logoURL.flatMap { URL(string: $0) },
            primaryColor: primaryColor,
            secondaryColor: secondaryColor
        )
    }
}

// MARK: - Team Alias

@Model
final class TeamAlias {
    @Attribute(.unique) var id: String
    var teamCanonicalId: String
    var aliasTypeRaw: String
    var aliasValue: String
    var validFrom: Date?
    var validUntil: Date?
    var schemaVersion: Int
    var lastModified: Date

    var team: CanonicalTeam?

    init(
        id: String,
        teamCanonicalId: String,
        aliasType: TeamAliasType,
        aliasValue: String,
        validFrom: Date? = nil,
        validUntil: Date? = nil,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date()
    ) {
        self.id = id
        self.teamCanonicalId = teamCanonicalId
        self.aliasTypeRaw = aliasType.rawValue
        self.aliasValue = aliasValue
        self.validFrom = validFrom
        self.validUntil = validUntil
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
    }

    var aliasType: TeamAliasType {
        get { TeamAliasType(rawValue: aliasTypeRaw) ?? .name }
        set { aliasTypeRaw = newValue.rawValue }
    }
}

// MARK: - League Structure

@Model
final class LeagueStructureModel {
    @Attribute(.unique) var id: String
    var sport: String
    var structureTypeRaw: String
    var name: String
    var abbreviation: String?
    var parentId: String?
    var displayOrder: Int
    var schemaVersion: Int
    var lastModified: Date

    init(
        id: String,
        sport: String,
        structureType: LeagueStructureType,
        name: String,
        abbreviation: String? = nil,
        parentId: String? = nil,
        displayOrder: Int = 0,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date()
    ) {
        self.id = id
        self.sport = sport
        self.structureTypeRaw = structureType.rawValue
        self.name = name
        self.abbreviation = abbreviation
        self.parentId = parentId
        self.displayOrder = displayOrder
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
    }

    var structureType: LeagueStructureType {
        get { LeagueStructureType(rawValue: structureTypeRaw) ?? .division }
        set { structureTypeRaw = newValue.rawValue }
    }

    var sportEnum: Sport? { Sport(rawValue: sport) }
}

// MARK: - Canonical Game

@Model
final class CanonicalGame {
    @Attribute(.unique) var canonicalId: String
    var uuid: UUID

    var schemaVersion: Int
    var lastModified: Date
    var sourceRaw: String

    var deprecatedAt: Date?
    var deprecationReason: String?
    var rescheduledToCanonicalId: String?

    var homeTeamCanonicalId: String
    var awayTeamCanonicalId: String
    var stadiumCanonicalId: String
    var dateTime: Date
    var sport: String
    var season: String
    var isPlayoff: Bool
    var broadcastInfo: String?

    // User-correctable
    var userAttending: Bool = false
    var userNotes: String?

    init(
        canonicalId: String,
        uuid: UUID? = nil,
        schemaVersion: Int = SchemaVersion.current,
        lastModified: Date = Date(),
        source: DataSource = .bundled,
        homeTeamCanonicalId: String,
        awayTeamCanonicalId: String,
        stadiumCanonicalId: String,
        dateTime: Date,
        sport: String,
        season: String,
        isPlayoff: Bool = false,
        broadcastInfo: String? = nil
    ) {
        self.canonicalId = canonicalId
        self.uuid = uuid ?? CanonicalStadium.deterministicUUID(from: canonicalId)
        self.schemaVersion = schemaVersion
        self.lastModified = lastModified
        self.sourceRaw = source.rawValue
        self.homeTeamCanonicalId = homeTeamCanonicalId
        self.awayTeamCanonicalId = awayTeamCanonicalId
        self.stadiumCanonicalId = stadiumCanonicalId
        self.dateTime = dateTime
        self.sport = sport
        self.season = season
        self.isPlayoff = isPlayoff
        self.broadcastInfo = broadcastInfo
    }

    var source: DataSource {
        get { DataSource(rawValue: sourceRaw) ?? .bundled }
        set { sourceRaw = newValue.rawValue }
    }

    var isActive: Bool { deprecatedAt == nil }

    var sportEnum: Sport? { Sport(rawValue: sport) }

    func toDomain() -> Game {
        Game(
            id: canonicalId,
            homeTeamId: homeTeamCanonicalId,
            awayTeamId: awayTeamCanonicalId,
            stadiumId: stadiumCanonicalId,
            dateTime: dateTime,
            sport: sportEnum ?? .mlb,
            season: season,
            isPlayoff: isPlayoff,
            broadcastInfo: broadcastInfo
        )
    }
}

// MARK: - Canonical Sport

@Model
final class CanonicalSport {
    @Attribute(.unique) var id: String
    var abbreviation: String
    var displayName: String
    var iconName: String
    var colorHex: String
    var seasonStartMonth: Int
    var seasonEndMonth: Int
    var isActive: Bool
    var lastModified: Date
    var schemaVersion: Int
    var sourceRaw: String

    init(
        id: String,
        abbreviation: String,
        displayName: String,
        iconName: String,
        colorHex: String,
        seasonStartMonth: Int,
        seasonEndMonth: Int,
        isActive: Bool = true,
        lastModified: Date = Date(),
        schemaVersion: Int = SchemaVersion.current,
        source: DataSource = .cloudKit
    ) {
        self.id = id
        self.abbreviation = abbreviation
        self.displayName = displayName
        self.iconName = iconName
        self.colorHex = colorHex
        self.seasonStartMonth = seasonStartMonth
        self.seasonEndMonth = seasonEndMonth
        self.isActive = isActive
        self.lastModified = lastModified
        self.schemaVersion = schemaVersion
        self.sourceRaw = source.rawValue
    }

    var source: DataSource {
        get { DataSource(rawValue: sourceRaw) ?? .cloudKit }
        set { sourceRaw = newValue.rawValue }
    }

    func toDomain() -> DynamicSport {
        DynamicSport(
            id: id,
            abbreviation: abbreviation,
            displayName: displayName,
            iconName: iconName,
            colorHex: colorHex,
            seasonStartMonth: seasonStartMonth,
            seasonEndMonth: seasonEndMonth
        )
    }
}

// MARK: - Bundled Data Timestamps

/// Timestamps for bundled data files.
/// Marked nonisolated to allow access from any isolation domain.
nonisolated enum BundledDataTimestamp {
    static let stadiums = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
    static let games = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
    static let leagueStructure = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
    static let sports = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
}
