import AVFoundation
import Foundation
import Observation

/// Drives "read aloud" mode for `BookReaderView`. Wraps an
/// `AVSpeechSynthesizer` with a queue of paragraph utterances and exposes the
/// current paragraph/word index so the view can highlight the active word.
///
/// Skips vocabulary lines (`palabra = meaning`) since the synth pronounces the
/// `=` awkwardly and the bilingual gloss is reference material, not prose.
@MainActor
@Observable
final class BookSpeechController: NSObject, AVSpeechSynthesizerDelegate {
    // MARK: - Observable state

    private(set) var isReading: Bool = false
    private(set) var isPaused: Bool = false
    private(set) var currentParagraphIndex: Int? = nil
    private(set) var currentWordIndex: Int? = nil

    // MARK: - Configuration

    var rate: Float = 0.45
    var voiceIdentifier: String? = nil

    // MARK: - Internals

    /// Built on first use, not in `init`. `AVSpeechSynthesizer()` connects to
    /// the system speech daemon, so allocating one per `BookReaderView` struct
    /// construction (SwiftUI rebuilds the struct on every parent render) is a
    /// real cost — deferring it keeps controller construction cheap.
    @ObservationIgnored
    private lazy var synthesizer: AVSpeechSynthesizer = {
        let synth = AVSpeechSynthesizer()
        synth.delegate = self
        return synth
    }()
    private var queue: [QueueEntry] = []
    private var queueCursor: Int = 0
    private var audioSessionConfigured = false

    private struct QueueEntry {
        let paragraphIndex: Int
        let text: String
        let wordRanges: [Range<String.Index>]
    }

    override init() {
        super.init()
    }

    // MARK: - Public control

    /// Start (or restart) reading the given paragraphs. Indexes in
    /// `currentParagraphIndex` are positions in the original `paragraphs`
    /// array — vocab lines are skipped internally but the visible index space
    /// matches what the caller passed.
    func start(paragraphs: [String], from startIndex: Int = 0) {
        stop()
        configureAudioSession()

        var entries: [QueueEntry] = []
        for (idx, p) in paragraphs.enumerated() where idx >= startIndex {
            if Self.isVocabLine(p) { continue }
            entries.append(QueueEntry(
                paragraphIndex: idx,
                text: p,
                wordRanges: Self.wordRanges(in: p)
            ))
        }
        guard !entries.isEmpty else { return }

        queue = entries
        queueCursor = 0
        isReading = true
        isPaused = false
        speakCurrent()
    }

    /// Pause immediately (no word boundary). Use this for tap-to-define so the
    /// audio stops the moment the user taps.
    func pause() {
        guard isReading, !isPaused else { return }
        synthesizer.pauseSpeaking(at: .immediate)
        isPaused = true
    }

    func resume() {
        guard isReading, isPaused else { return }
        synthesizer.continueSpeaking()
        isPaused = false
    }

    func stop() {
        synthesizer.stopSpeaking(at: .immediate)
        queue.removeAll()
        queueCursor = 0
        isReading = false
        isPaused = false
        currentParagraphIndex = nil
        currentWordIndex = nil
        deactivateAudioSession()
    }

    // MARK: - Vocab detection + word ranges

    /// Vocabulary entries in the book are formatted `palabra = meaning`.
    /// Reading them aloud says "palabra equals meaning" which is awkward, and
    /// they're reference material, so the read-along skips them.
    static func isVocabLine(_ paragraph: String) -> Bool {
        paragraph.contains(" = ")
    }

    /// Word ranges that match the BookReaderView's space-split rendering —
    /// the visible word index N in a paragraph corresponds to wordRanges[N].
    static func wordRanges(in text: String) -> [Range<String.Index>] {
        var ranges: [Range<String.Index>] = []
        var i = text.startIndex
        while i < text.endIndex {
            while i < text.endIndex && text[i] == " " {
                i = text.index(after: i)
            }
            guard i < text.endIndex else { break }
            let start = i
            while i < text.endIndex && text[i] != " " {
                i = text.index(after: i)
            }
            ranges.append(start..<i)
        }
        return ranges
    }

    // MARK: - Private

    private func speakCurrent() {
        guard queueCursor < queue.count else {
            stop()
            return
        }
        let entry = queue[queueCursor]
        currentParagraphIndex = entry.paragraphIndex
        currentWordIndex = nil

        let utterance = AVSpeechUtterance(string: entry.text)
        utterance.voice = resolveVoice()
        utterance.rate = rate
        utterance.pitchMultiplier = 1.0
        utterance.postUtteranceDelay = 0.20
        synthesizer.speak(utterance)
    }

    private func resolveVoice() -> AVSpeechSynthesisVoice? {
        if let id = voiceIdentifier, let v = AVSpeechSynthesisVoice(identifier: id) {
            return v
        }
        return AVSpeechSynthesisVoice(language: "es-ES")
    }

    private func configureAudioSession() {
        guard !audioSessionConfigured else { return }
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playback, mode: .spokenAudio, options: [])
            try session.setActive(true)
            audioSessionConfigured = true
        } catch {
            print("[BookSpeech] audio session failed: \(error)")
        }
    }

    /// Release audio focus on stop so the OS hands control back to whatever
    /// app was playing before (music, podcast, etc.). Without this the
    /// session stays "active" until the app is killed.
    private func deactivateAudioSession() {
        guard audioSessionConfigured else { return }
        do {
            try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
        } catch {
            print("[BookSpeech] audio session deactivation failed: \(error)")
        }
        audioSessionConfigured = false
    }

    private func handleWillSpeakRange(_ range: NSRange) {
        guard queueCursor < queue.count else { return }
        let entry = queue[queueCursor]
        guard let stringRange = Range(range, in: entry.text) else { return }
        let lower = stringRange.lowerBound
        let idx = entry.wordRanges.firstIndex {
            $0.lowerBound <= lower && lower < $0.upperBound
        }
        if let idx, idx != currentWordIndex {
            currentWordIndex = idx
        }
    }

    private func handleDidFinish() {
        queueCursor += 1
        if queueCursor < queue.count {
            speakCurrent()
        } else {
            stop()
        }
    }

    // MARK: - AVSpeechSynthesizerDelegate

    nonisolated func speechSynthesizer(
        _ synthesizer: AVSpeechSynthesizer,
        willSpeakRangeOfSpeechString characterRange: NSRange,
        utterance: AVSpeechUtterance
    ) {
        Task { @MainActor in self.handleWillSpeakRange(characterRange) }
    }

    nonisolated func speechSynthesizer(
        _ synthesizer: AVSpeechSynthesizer,
        didFinish utterance: AVSpeechUtterance
    ) {
        Task { @MainActor in self.handleDidFinish() }
    }
}
