import Foundation
import Observation
import SwiftUI

@MainActor
@Observable
final class GameViewModel {
    // MARK: - Dependencies

    private(set) var rules: GameRules
    private let soundManager = SoundManager()
    private let timerService = TimerService()

    // MARK: - Persistence

    var persistenceManager: PersistenceManager?
    private var autoSaveTask: Task<Void, Never>?

    // MARK: - State

    private(set) var state = GameState()
    private(set) var variant: GameVariant
    var difficulty: Difficulty

    // MARK: - Drag state

    var draggedCards: [Card] = []
    var dragSource: CardLocation?
    var dragPosition: CGPoint = .zero
    var dropTargets: [DropTargetData] = []

    // MARK: - Hint state

    var currentHint: HintResult?
    var isShowingHint: Bool = false

    // MARK: - Timer

    var elapsedSeconds: Int { timerService.elapsedSeconds }

    // MARK: - Computed

    var canUndo: Bool {
        state.canUndo && undosRemaining > 0
    }

    var isWon: Bool { state.phase == .won }

    private var undosRemaining: Int

    // MARK: - Init

    init(variant: GameVariant = .klondike, difficulty: Difficulty = .medium) {
        self.variant = variant
        self.difficulty = difficulty
        self.rules = GameRulesFactory.rules(for: variant)
        self.undosRemaining = difficulty.settings.maxUndos
    }

    // MARK: - Game Actions

    func newGame() {
        // Record loss for previous game if it was in progress
        if state.phase == .playing {
            persistenceManager?.recordLoss(variant: variant, difficulty: difficulty)
        }

        rules = GameRulesFactory.rules(for: variant)
        let deck = variant.deckCount == 2 ? Deck.double() : Deck.standard()
        let snapshot = rules.deal(deck: deck)
        state.reset(from: snapshot, variant: variant)
        undosRemaining = difficulty.settings.maxUndos
        timerService.reset()
        timerService.start()
        playSound(.cardFlip)
        scheduleAutoSave()
    }

    func changeVariant(to newVariant: GameVariant) {
        variant = newVariant
        newGame()
    }

    /// Resume a saved game from a GameRecord.
    func resumeGame(from record: GameRecord) {
        guard let snapshot = record.decodedSnapshot,
              let savedVariant = record.gameVariant,
              let savedDifficulty = record.gameDifficulty else { return }

        variant = savedVariant
        difficulty = savedDifficulty
        rules = GameRulesFactory.rules(for: variant)

        state.restore(from: snapshot)
        state.phase = .playing
        if variant.hasFreeCells && state.freeCells.isEmpty {
            state.freeCells = Array(repeating: nil, count: variant.freeCellCount)
        }

        undosRemaining = difficulty.settings.maxUndos
        timerService.reset()
        timerService.elapsedSeconds = record.elapsedSeconds
        timerService.start()
    }

    func tapCard(at location: CardLocation, cardIndex: Int) {
        guard state.phase == .playing else { return }

        switch location {
        case .stock:
            drawFromStock()
        case .waste:
            guard let card = state.waste.last else { return }
            if let dest = findBestDestination(for: [card], from: .waste) {
                executeMove(cards: [card], from: .waste, to: dest)
            }
        case .tableau(let tabIndex):
            let tableau = state.tableaus[tabIndex]
            guard cardIndex == tableau.count - 1, let card = tableau.last, card.isFaceUp else { return }
            if let dest = findBestDestination(for: [card], from: location) {
                executeMove(cards: [card], from: location, to: dest)
            }
        case .freeCell(let cellIndex):
            guard let card = state.freeCells[cellIndex] else { return }
            if let dest = findBestDestination(for: [card], from: location) {
                executeMove(cards: [card], from: location, to: dest)
            }
        case .foundation:
            break
        }
    }

    func beginDrag(cards: [Card], from: CardLocation) {
        guard state.phase == .playing else { return }
        guard rules.canPickUp(cards: cards, from: from, state: state.snapshot()) else { return }
        draggedCards = cards
        dragSource = from
    }

    func drop(at destination: CardLocation) -> Bool {
        guard let source = dragSource, !draggedCards.isEmpty else {
            cancelDrag()
            return false
        }

        let snapshot = state.snapshot()
        guard rules.canMove(cards: draggedCards, from: source, to: destination, state: snapshot) else {
            cancelDrag()
            return false
        }

        executeMove(cards: draggedCards, from: source, to: destination)
        clearDrag()
        return true
    }

    func cancelDrag() {
        clearDrag()
    }

    /// Attempt to drop at the given point in the board coordinate space.
    /// Falls back to cancel if no valid target found.
    func endDrag(at point: CGPoint) {
        guard !draggedCards.isEmpty else {
            clearDrag()
            return
        }

        // Find the drop target under the finger
        let candidates = dropTargets.filter { $0.frame.contains(point) }
        let target = candidates.min(by: {
            $0.frame.width * $0.frame.height < $1.frame.width * $1.frame.height
        })

        if let target {
            _ = drop(at: target.location)
        } else {
            cancelDrag()
        }
    }

    func undo() {
        guard canUndo, let snapshot = state.popHistory() else { return }
        state.restore(from: snapshot)
        undosRemaining -= 1
        playSound(.cardFlip)
        scheduleAutoSave()
    }

    func requestHint() {
        let hints = rules.findHints(state: state.snapshot(), settings: difficulty.settings)
        currentHint = hints.first
        if currentHint != nil {
            isShowingHint = true
        }
    }

    func drawFromStock() {
        guard state.phase == .playing else { return }
        state.pushHistory()
        var snapshot = state.snapshot()
        if rules.drawFromStock(state: &snapshot, drawCount: difficulty.settings.drawCount) != nil {
            state.restore(from: snapshot)
            state.moves += 1
            playSound(.cardFlip)
            scheduleAutoSave()
        }
    }

    func autoComplete() {
        guard state.phase == .playing else { return }
        guard rules.canAutoComplete(state: state.snapshot()) else { return }
        state.phase = .autoCompleting

        Task {
            while state.phase == .autoCompleting {
                guard let (from, to) = AutoCompleter.findNextAutoMove(state: state.snapshot(), rules: rules) else {
                    break
                }
                guard let card = cardAt(location: from) else { break }
                executeMove(cards: [card], from: from, to: to)
                try? await Task.sleep(for: .milliseconds(150))
            }

            if rules.isWon(state: state.snapshot()) {
                handleWin()
            } else {
                state.phase = .playing
            }
        }
    }

    func pause() {
        guard state.phase == .playing else { return }
        state.phase = .paused
        timerService.stop()
        saveGameNow()
    }

    func resume() {
        guard state.phase == .paused else { return }
        state.phase = .playing
        timerService.start()
    }

    /// Save game state immediately (called on app background/close).
    func saveGameNow() {
        guard state.phase == .playing || state.phase == .paused else { return }
        persistenceManager?.saveGame(
            variant: variant,
            difficulty: difficulty,
            snapshot: state.snapshot(),
            elapsedSeconds: timerService.elapsedSeconds
        )
    }

    var isSoundEnabled: Bool {
        get { soundManager.isEnabled }
        set { soundManager.isEnabled = newValue }
    }

    // MARK: - Private

    private func cardAt(location: CardLocation) -> Card? {
        switch location {
        case .tableau(let i): state.tableaus[i].last
        case .waste: state.waste.last
        case .freeCell(let i): state.freeCells[i]
        case .foundation(let i): state.foundations[i].last
        case .stock: state.stock.last
        }
    }

    private func executeMove(cards: [Card], from: CardLocation, to: CardLocation) {
        state.pushHistory()

        // Remove cards from source
        switch from {
        case .tableau(let index):
            state.tableaus[index].removeLast(cards.count)
            if let lastIndex = state.tableaus[index].indices.last,
               !state.tableaus[index][lastIndex].isFaceUp {
                state.tableaus[index][lastIndex].isFaceUp = true
            }
        case .waste:
            state.waste.removeLast()
        case .freeCell(let index):
            state.freeCells[index] = nil
        case .foundation(let index):
            state.foundations[index].removeLast()
        case .stock:
            break
        }

        // Add cards to destination
        switch to {
        case .tableau(let index):
            state.tableaus[index].append(contentsOf: cards)
        case .foundation(let index):
            state.foundations[index].append(contentsOf: cards)
        case .freeCell(let index):
            state.freeCells[index] = cards.first
        case .waste:
            state.waste.append(contentsOf: cards)
        case .stock:
            break
        }

        state.moves += 1
        state.score += rules.scoreForMove(from: from, to: to)

        // Check for Spider complete sequences
        if variant == .spider, let spiderRules = rules as? SpiderRules {
            var snapshot = state.snapshot()
            if spiderRules.checkAndMoveCompleteSequences(state: &snapshot) {
                state.restore(from: snapshot)
                playSound(.cardPlace)
            }
        }

        // Check win
        if rules.isWon(state: state.snapshot()) {
            handleWin()
        } else {
            playSound(.cardPlace)
            scheduleAutoSave()
        }
    }

    private func handleWin() {
        state.phase = .won
        timerService.stop()
        playSound(.victory)

        // Record win in statistics
        persistenceManager?.recordWin(
            variant: variant,
            difficulty: difficulty,
            score: state.score,
            time: timerService.elapsedSeconds
        )

        // Delete saved game (game is over)
        persistenceManager?.deleteSavedGame(for: variant)
    }

    /// Debounced auto-save: waits 2 seconds of inactivity before saving.
    private func scheduleAutoSave() {
        autoSaveTask?.cancel()
        autoSaveTask = Task {
            try? await Task.sleep(for: .seconds(2))
            guard !Task.isCancelled else { return }
            saveGameNow()
        }
    }

    private func findBestDestination(for cards: [Card], from: CardLocation) -> CardLocation? {
        let destinations = rules.validDestinations(for: cards, from: from, state: state.snapshot())
        return destinations.first { if case .foundation = $0 { return true }; return false }
            ?? destinations.first
    }

    private func clearDrag() {
        draggedCards = []
        dragSource = nil
        dragPosition = .zero
    }

    private nonisolated func playSound(_ effect: SoundEffect) {
        Task { await soundManager.play(effect) }
    }
}
