//
//  StadiumIdentityService.swift
//  SportsTime
//
//  Service for resolving stadium identities across renames and aliases.
//  Wraps CanonicalStadium lookups from SwiftData.
//

import Foundation
import SwiftData

// MARK: - Stadium Identity Service

/// Resolves stadium identities to canonical IDs, handling renames and aliases.
/// Example: "SBC Park", "AT&T Park", and "Oracle Park" all resolve to the same canonical ID.
actor StadiumIdentityService {

    // MARK: - Singleton

    static let shared = StadiumIdentityService()

    // MARK: - Properties

    private var modelContainer: ModelContainer?

    // Cache for performance
    private var uuidToCanonicalId: [UUID: String] = [:]
    private var canonicalIdToUUID: [String: UUID] = [:]
    private var nameToCanonicalId: [String: String] = [:]

    // MARK: - Initialization

    private init() {}

    // MARK: - Configuration

    /// Configure the service with a model container
    func configure(with container: ModelContainer) {
        self.modelContainer = container
        invalidateCache()
    }

    // MARK: - Public Methods

    /// Get the canonical ID for a stadium UUID
    /// Returns the same canonicalId for stadiums that are the same physical location
    func canonicalId(for stadiumUUID: UUID) async throws -> String? {
        // Check cache first
        if let cached = uuidToCanonicalId[stadiumUUID] {
            return cached
        }

        guard let container = modelContainer else {
            return nil
        }

        let context = ModelContext(container)

        let descriptor = FetchDescriptor<CanonicalStadium>(
            predicate: #Predicate<CanonicalStadium> { stadium in
                stadium.uuid == stadiumUUID
            }
        )

        guard let stadium = try context.fetch(descriptor).first else {
            return nil
        }

        // Cache the result
        uuidToCanonicalId[stadiumUUID] = stadium.canonicalId
        canonicalIdToUUID[stadium.canonicalId] = stadium.uuid

        return stadium.canonicalId
    }

    /// Get the canonical ID for a stadium name (searches aliases too)
    func canonicalId(forName name: String) async throws -> String? {
        let lowercasedName = name.lowercased()

        // Check cache first
        if let cached = nameToCanonicalId[lowercasedName] {
            return cached
        }

        guard let container = modelContainer else {
            return nil
        }

        let context = ModelContext(container)

        // First check stadium aliases
        let aliasDescriptor = FetchDescriptor<StadiumAlias>(
            predicate: #Predicate<StadiumAlias> { alias in
                alias.aliasName == lowercasedName
            }
        )

        if let alias = try context.fetch(aliasDescriptor).first {
            nameToCanonicalId[lowercasedName] = alias.stadiumCanonicalId
            return alias.stadiumCanonicalId
        }

        // Fall back to direct stadium name match
        let stadiumDescriptor = FetchDescriptor<CanonicalStadium>()
        let stadiums = try context.fetch(stadiumDescriptor)

        // Case-insensitive match on stadium name
        if let stadium = stadiums.first(where: { $0.name.lowercased() == lowercasedName }) {
            nameToCanonicalId[lowercasedName] = stadium.canonicalId
            return stadium.canonicalId
        }

        return nil
    }

    /// Check if two stadium UUIDs represent the same physical stadium
    func isSameStadium(_ id1: UUID, _ id2: UUID) async throws -> Bool {
        guard let canonicalId1 = try await canonicalId(for: id1),
              let canonicalId2 = try await canonicalId(for: id2) else {
            // If we can't resolve, fall back to direct comparison
            return id1 == id2
        }
        return canonicalId1 == canonicalId2
    }

    /// Get the current UUID for a canonical stadium ID
    func currentUUID(forCanonicalId canonicalId: String) async throws -> UUID? {
        // Check cache first
        if let cached = canonicalIdToUUID[canonicalId] {
            return cached
        }

        guard let container = modelContainer else {
            return nil
        }

        let context = ModelContext(container)

        let descriptor = FetchDescriptor<CanonicalStadium>(
            predicate: #Predicate<CanonicalStadium> { stadium in
                stadium.canonicalId == canonicalId && stadium.deprecatedAt == nil
            }
        )

        guard let stadium = try context.fetch(descriptor).first else {
            return nil
        }

        // Cache the result
        canonicalIdToUUID[canonicalId] = stadium.uuid
        uuidToCanonicalId[stadium.uuid] = stadium.canonicalId

        return stadium.uuid
    }

    /// Get the current name for a canonical stadium ID
    func currentName(forCanonicalId canonicalId: String) async throws -> String? {
        guard let container = modelContainer else {
            return nil
        }

        let context = ModelContext(container)

        let descriptor = FetchDescriptor<CanonicalStadium>(
            predicate: #Predicate<CanonicalStadium> { stadium in
                stadium.canonicalId == canonicalId && stadium.deprecatedAt == nil
            }
        )

        guard let stadium = try context.fetch(descriptor).first else {
            return nil
        }

        return stadium.name
    }

    /// Get all historical names for a stadium
    func allNames(forCanonicalId canonicalId: String) async throws -> [String] {
        guard let container = modelContainer else {
            return []
        }

        let context = ModelContext(container)

        // Get aliases
        let aliasDescriptor = FetchDescriptor<StadiumAlias>(
            predicate: #Predicate<StadiumAlias> { alias in
                alias.stadiumCanonicalId == canonicalId
            }
        )

        let aliases = try context.fetch(aliasDescriptor)
        var names = aliases.map { $0.aliasName }

        // Add current name
        if let currentName = try await currentName(forCanonicalId: canonicalId) {
            if !names.contains(currentName.lowercased()) {
                names.append(currentName)
            }
        }

        return names
    }

    /// Find stadium by approximate location (for photo import)
    func findStadium(near latitude: Double, longitude: Double, radiusMeters: Double = 5000) async throws -> CanonicalStadium? {
        guard let container = modelContainer else {
            return nil
        }

        let context = ModelContext(container)

        // Fetch all active stadiums and filter by distance
        let descriptor = FetchDescriptor<CanonicalStadium>(
            predicate: #Predicate<CanonicalStadium> { stadium in
                stadium.deprecatedAt == nil
            }
        )

        let stadiums = try context.fetch(descriptor)

        // Calculate approximate degree ranges for the radius
        // At equator: 1 degree ≈ 111km, so radiusMeters / 111000 gives degrees
        let degreeDelta = radiusMeters / 111000.0

        let nearbyStadiums = stadiums.filter { stadium in
            abs(stadium.latitude - latitude) <= degreeDelta &&
            abs(stadium.longitude - longitude) <= degreeDelta * 1.5 // Account for longitude compression at higher latitudes
        }

        // If multiple stadiums nearby, find the closest
        guard !nearbyStadiums.isEmpty else {
            return nil
        }

        if nearbyStadiums.count == 1 {
            return nearbyStadiums.first
        }

        // Calculate actual distances for the nearby stadiums
        return nearbyStadiums.min { s1, s2 in
            let d1 = haversineDistance(lat1: latitude, lon1: longitude, lat2: s1.latitude, lon2: s1.longitude)
            let d2 = haversineDistance(lat1: latitude, lon1: longitude, lat2: s2.latitude, lon2: s2.longitude)
            return d1 < d2
        }
    }

    // MARK: - Cache Management

    /// Invalidate all caches (call after sync)
    func invalidateCache() {
        uuidToCanonicalId.removeAll()
        canonicalIdToUUID.removeAll()
        nameToCanonicalId.removeAll()
    }

    // MARK: - Private Helpers

    /// Calculate distance between two coordinates using Haversine formula
    nonisolated private func haversineDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> Double {
        let R = 6371000.0 // Earth's radius in meters
        let phi1 = lat1 * .pi / 180
        let phi2 = lat2 * .pi / 180
        let deltaPhi = (lat2 - lat1) * .pi / 180
        let deltaLambda = (lon2 - lon1) * .pi / 180

        let a = sin(deltaPhi / 2) * sin(deltaPhi / 2) +
                cos(phi1) * cos(phi2) * sin(deltaLambda / 2) * sin(deltaLambda / 2)
        let c = 2 * atan2(sqrt(a), sqrt(1 - a))

        return R * c
    }
}
