//
//  PollService.swift
//  SportsTime
//
//  CloudKit service for trip polls and voting
//

import Foundation
import CloudKit
import SwiftData

// MARK: - Poll Errors

enum PollError: Error, LocalizedError {
    case notSignedIn
    case pollNotFound
    case alreadyVoted
    case notPollOwner
    case networkUnavailable
    case encodingError
    case unknown(Error)

    var errorDescription: String? {
        switch self {
        case .notSignedIn:
            return "Please sign in to iCloud to use polls."
        case .pollNotFound:
            return "Poll not found. It may have been deleted."
        case .alreadyVoted:
            return "You have already voted on this poll."
        case .notPollOwner:
            return "Only the poll owner can perform this action."
        case .networkUnavailable:
            return "Unable to connect. Please check your internet connection."
        case .encodingError:
            return "Failed to save poll data."
        case .unknown(let error):
            return "An error occurred: \(error.localizedDescription)"
        }
    }
}

// MARK: - Poll Service

actor PollService {
    static let shared = PollService()

    private let container: CKContainer
    private let publicDatabase: CKDatabase

    private var currentUserRecordID: String?
    private var pollSubscriptionID: CKSubscription.ID?

    private init() {
        self.container = CloudKitContainerConfig.makeContainer()
        self.publicDatabase = container.publicCloudDatabase
    }

    // MARK: - User Identity

    func getCurrentUserRecordID() async throws -> String {
        if let cached = currentUserRecordID {
            return cached
        }

        let recordID = try await container.userRecordID()
        currentUserRecordID = recordID.recordName
        return recordID.recordName
    }

    func isSignedIn() async -> Bool {
        do {
            _ = try await getCurrentUserRecordID()
            return true
        } catch {
            return false
        }
    }

    // MARK: - Poll CRUD

    func createPoll(_ poll: TripPoll) async throws -> TripPoll {
        let ckPoll = CKTripPoll(poll: poll)

        do {
            try await publicDatabase.save(ckPoll.record)
            return poll
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func fetchPoll(byShareCode shareCode: String) async throws -> TripPoll {
        let predicate = NSPredicate(format: "%K == %@", CKTripPoll.shareCodeKey, shareCode.uppercased())
        let query = CKQuery(recordType: CKRecordType.tripPoll, predicate: predicate)

        do {
            let (results, _) = try await publicDatabase.records(matching: query)

            guard let result = results.first,
                  case .success(let record) = result.1,
                  let poll = CKTripPoll(record: record).toPoll()
            else {
                throw PollError.pollNotFound
            }

            return poll
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch let error as PollError {
            throw error
        } catch {
            throw PollError.unknown(error)
        }
    }

    func fetchPoll(byId pollId: UUID) async throws -> TripPoll {
        let recordID = CKRecord.ID(recordName: pollId.uuidString)

        do {
            let record = try await publicDatabase.record(for: recordID)
            guard let poll = CKTripPoll(record: record).toPoll() else {
                throw PollError.pollNotFound
            }
            return poll
        } catch let error as CKError {
            if error.code == .unknownItem {
                throw PollError.pollNotFound
            }
            throw mapCloudKitError(error)
        } catch let error as PollError {
            throw error
        } catch {
            throw PollError.unknown(error)
        }
    }

    func fetchMyPolls() async throws -> [TripPoll] {
        let userId = try await getCurrentUserRecordID()
        let predicate = NSPredicate(format: "%K == %@", CKTripPoll.ownerIdKey, userId)
        let query = CKQuery(recordType: CKRecordType.tripPoll, predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: CKTripPoll.createdAtKey, ascending: false)]

        do {
            let (results, _) = try await publicDatabase.records(matching: query)

            return results.compactMap { result in
                guard case .success(let record) = result.1 else { return nil }
                return CKTripPoll(record: record).toPoll()
            }
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func updatePoll(_ poll: TripPoll, resetVotes: Bool = false) async throws -> TripPoll {
        let userId = try await getCurrentUserRecordID()
        guard poll.ownerId == userId else {
            throw PollError.notPollOwner
        }

        var updatedPoll = poll
        updatedPoll.modifiedAt = Date()

        let ckPoll = CKTripPoll(poll: updatedPoll)

        do {
            try await publicDatabase.save(ckPoll.record)

            if resetVotes {
                try await deleteVotes(forPollId: poll.id)
            }

            return updatedPoll
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func deletePoll(_ pollId: UUID) async throws {
        // Verify ownership before deleting
        let poll = try await fetchPoll(byId: pollId)
        let userId = try await getCurrentUserRecordID()
        guard poll.ownerId == userId else {
            throw PollError.notPollOwner
        }

        let recordID = CKRecord.ID(recordName: pollId.uuidString)

        do {
            // Delete all votes first
            try await deleteVotes(forPollId: pollId)
            // Then delete the poll
            try await publicDatabase.deleteRecord(withID: recordID)
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch let error as PollError {
            throw error
        } catch {
            throw PollError.unknown(error)
        }
    }

    // MARK: - Voting

    func submitVote(_ vote: PollVote) async throws -> PollVote {
        // Check if user already voted
        let existingVote = try await fetchMyVote(forPollId: vote.pollId)
        if existingVote != nil {
            throw PollError.alreadyVoted
        }

        let ckVote = CKPollVote(vote: vote)

        do {
            try await publicDatabase.save(ckVote.record)
            return vote
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func updateVote(_ vote: PollVote) async throws -> PollVote {
        let userId = try await getCurrentUserRecordID()
        guard vote.odg == userId else {
            throw PollError.notPollOwner  // Using this error for now - voter can only update own vote
        }

        // Fetch the existing record to get the server's changeTag
        let recordID = CKRecord.ID(recordName: vote.id.uuidString)
        let existingRecord: CKRecord
        do {
            existingRecord = try await publicDatabase.record(for: recordID)
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }

        // Update the fields on the fetched record
        let now = Date()
        existingRecord[CKPollVote.rankingsKey] = vote.rankings
        existingRecord[CKPollVote.modifiedAtKey] = now

        do {
            try await publicDatabase.save(existingRecord)
            var updatedVote = vote
            updatedVote.modifiedAt = now
            return updatedVote
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func fetchVotes(forPollId pollId: UUID) async throws -> [PollVote] {
        let predicate = NSPredicate(format: "%K == %@", CKPollVote.pollIdKey, pollId.uuidString)
        let query = CKQuery(recordType: CKRecordType.pollVote, predicate: predicate)

        do {
            let (results, _) = try await publicDatabase.records(matching: query)

            return results.compactMap { result in
                guard case .success(let record) = result.1 else { return nil }
                return CKPollVote(record: record).toVote()
            }
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    func fetchMyVote(forPollId pollId: UUID) async throws -> PollVote? {
        let userId = try await getCurrentUserRecordID()
        let predicate = NSPredicate(
            format: "%K == %@ AND %K == %@",
            CKPollVote.pollIdKey, pollId.uuidString,
            CKPollVote.voterIdKey, userId
        )
        let query = CKQuery(recordType: CKRecordType.pollVote, predicate: predicate)

        do {
            let (results, _) = try await publicDatabase.records(matching: query)

            guard let result = results.first,
                  case .success(let record) = result.1
            else {
                return nil
            }

            return CKPollVote(record: record).toVote()
        } catch let error as CKError {
            throw mapCloudKitError(error)
        } catch {
            throw PollError.unknown(error)
        }
    }

    private func deleteVotes(forPollId pollId: UUID) async throws {
        let votes = try await fetchVotes(forPollId: pollId)
        let recordIDs = votes.map { CKRecord.ID(recordName: $0.id.uuidString) }

        if !recordIDs.isEmpty {
            let result = try await publicDatabase.modifyRecords(saving: [], deleting: recordIDs)
            // Ignore individual delete errors - votes may already be deleted
            _ = result
        }
    }

    // MARK: - Subscriptions

    func subscribeToVoteUpdates(forPollId pollId: UUID) async throws {
        let predicate = NSPredicate(format: "%K == %@", CKPollVote.pollIdKey, pollId.uuidString)
        let subscription = CKQuerySubscription(
            recordType: CKRecordType.pollVote,
            predicate: predicate,
            subscriptionID: "poll-votes-\(pollId.uuidString)",
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        do {
            try await publicDatabase.save(subscription)
            pollSubscriptionID = subscription.subscriptionID
        } catch let error as CKError {
            // Subscription already exists is OK
            if error.code != .serverRejectedRequest {
                throw mapCloudKitError(error)
            }
        } catch {
            throw PollError.unknown(error)
        }
    }

    func unsubscribeFromVoteUpdates() async throws {
        guard let subscriptionID = pollSubscriptionID else { return }

        do {
            try await publicDatabase.deleteSubscription(withID: subscriptionID)
            pollSubscriptionID = nil
        } catch {
            // Ignore errors - subscription may not exist
        }
    }

    // MARK: - Results

    func fetchPollResults(forPollId pollId: UUID) async throws -> PollResults {
        async let pollTask = fetchPoll(byId: pollId)
        async let votesTask = fetchVotes(forPollId: pollId)

        let (poll, votes) = try await (pollTask, votesTask)

        return PollResults(poll: poll, votes: votes)
    }

    // MARK: - Error Mapping

    private func mapCloudKitError(_ error: CKError) -> PollError {
        switch error.code {
        case .notAuthenticated:
            return .notSignedIn
        case .networkUnavailable, .networkFailure:
            return .networkUnavailable
        case .unknownItem:
            return .pollNotFound
        default:
            return .unknown(error)
        }
    }
}
