import SwiftUI
import SharedModels
import SwiftData
import FoundationModels

struct BookReaderView: View {
    let chapter: BookChapter

    /// The book this chapter belongs to, resolved by slug — used for the
    /// pre-computed glossary. A @Query is safe here because the reader is
    /// built lazily by `navigationDestination`: one instance, when opened.
    @Query private var bookMatches: [Book]
    private var book: Book? { bookMatches.first }

    @Environment(DictionaryService.self) private var dictionary
    @State private var speech = BookSpeechController()
    @State private var selectedWord: WordAnnotation?
    @State private var showEnglish = false
    @State private var showVoicePicker = false
    @State private var wasReadingBeforeTap = false
    @State private var lookupCache: [String: WordAnnotation] = [:]
    /// The book's pre-computed glossary, decoded once on appear.
    @State private var glossary: [String: WordGloss] = [:]

    @AppStorage("bookReaderVoiceId") private var storedVoiceId: String = ""
    @AppStorage("bookReaderRate") private var storedRate: Double = 0.45

    init(chapter: BookChapter) {
        self.chapter = chapter
        let slug = chapter.bookSlug
        _bookMatches = Query(filter: #Predicate<Book> { $0.slug == slug })
    }

    private var paragraphsES: [String] { chapter.paragraphsES() }
    private var paragraphsEN: [String] { chapter.paragraphsEN() }

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 18) {
                    Text(chapter.title)
                        .font(.title2.bold())
                        .padding(.bottom, 4)
                        .id(-1)

                    ForEach(Array(paragraphsES.enumerated()), id: \.offset) { index, paragraph in
                        paragraphView(index: index, paragraph: paragraph)
                            .id(index)
                    }
                }
                .padding()
                .adaptiveContainer(maxWidth: 800)
            }
            .onChange(of: speech.currentParagraphIndex) { _, newIndex in
                guard let newIndex else { return }
                withAnimation(.easeInOut(duration: 0.25)) {
                    proxy.scrollTo(newIndex, anchor: .center)
                }
            }
        }
        .navigationTitle("Chapter \(chapter.number)")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItemGroup(placement: .topBarTrailing) {
                Button {
                    showVoicePicker = true
                } label: {
                    Image(systemName: "waveform.circle")
                        .symbolRenderingMode(.hierarchical)
                }
                .accessibilityLabel("Voice & speed")

                Button {
                    toggleReadAloud()
                } label: {
                    Image(systemName: speech.isReading ? "stop.circle.fill" : "play.circle.fill")
                        .symbolRenderingMode(.hierarchical)
                        .foregroundStyle(.indigo)
                }
                .accessibilityLabel(speech.isReading ? "Stop reading" : "Read aloud")

                Button {
                    withAnimation { showEnglish.toggle() }
                } label: {
                    Image(systemName: showEnglish ? "character.book.closed.fill.he" : "character.book.closed")
                        .symbolRenderingMode(.hierarchical)
                }
                .accessibilityLabel(showEnglish ? "Show Spanish" : "Show English")
            }
        }
        .sheet(item: $selectedWord, onDismiss: handleSheetDismiss) { word in
            WordDetailSheet(word: word)
                .presentationDetents([.height(220)])
        }
        .sheet(isPresented: $showVoicePicker) {
            BookVoicePickerSheet(voiceIdentifier: voiceBinding, rate: rateBinding)
        }
        .onAppear {
            speech.voiceIdentifier = storedVoiceId.isEmpty ? nil : storedVoiceId
            speech.rate = Float(storedRate)
            if glossary.isEmpty {
                glossary = book?.glossary() ?? [:]
            }
        }
        .onDisappear {
            speech.stop()
        }
    }

    @ViewBuilder
    private func paragraphView(index: Int, paragraph: String) -> some View {
        if showEnglish {
            Text(translation(for: index))
                .font(.body)
                .foregroundStyle(.secondary)
        } else {
            TappableParagraph(
                text: paragraph,
                highlightedWordIndex: speech.currentParagraphIndex == index ? speech.currentWordIndex : nil
            ) { word in
                handleTap(word: word, paragraph: paragraph)
            }
        }
    }

    private func translation(for index: Int) -> String {
        guard index < paragraphsEN.count else { return "" }
        let en = paragraphsEN[index]
        return en.isEmpty ? "[translation unavailable]" : en
    }

    // MARK: - Read-along controls

    private func toggleReadAloud() {
        if speech.isReading {
            speech.stop()
        } else {
            // Start from the first non-vocab paragraph at or after the topmost
            // visible one. For V1 we start from the chapter top — adding
            // "start from visible paragraph" would need a scroll-position
            // observer, which isn't worth the complexity yet.
            speech.start(paragraphs: paragraphsES)
        }
    }

    private var voiceBinding: Binding<String?> {
        Binding(
            get: { storedVoiceId.isEmpty ? nil : storedVoiceId },
            set: { newValue in
                storedVoiceId = newValue ?? ""
                speech.voiceIdentifier = newValue
            }
        )
    }

    private var rateBinding: Binding<Float> {
        Binding(
            get: { Float(storedRate) },
            set: { newValue in
                storedRate = Double(newValue)
                speech.rate = newValue
            }
        )
    }

    // MARK: - Word tap → definition

    private func handleTap(word: String, paragraph: String) {
        let cleaned = cleanWord(word)
        if cleaned.isEmpty { return }

        // If reading aloud, pause immediately. Remember so we can resume when
        // the user dismisses the definition sheet.
        if speech.isReading, !speech.isPaused {
            speech.pause()
            wasReadingBeforeTap = true
        }

        // Fall-through chain, best source first. Whichever resource answers,
        // the popup names it so a curated glossary hit reads differently from
        // a best-effort on-device LLM guess.
        if let cached = lookupCache[cleaned] {
            selectedWord = cached
            return
        }
        if let gloss = glossary[cleaned] {
            let annotation = WordAnnotation(
                word: cleaned,
                baseForm: gloss.baseForm,
                english: gloss.english,
                partOfSpeech: gloss.partOfSpeech,
                source: "Book glossary"
            )
            lookupCache[cleaned] = annotation
            selectedWord = annotation
            return
        }
        if let entry = dictionary.lookup(cleaned) {
            let annotation = WordAnnotation(
                word: cleaned,
                baseForm: entry.baseForm,
                english: entry.english,
                partOfSpeech: entry.partOfSpeech,
                source: "Dictionary"
            )
            lookupCache[cleaned] = annotation
            selectedWord = annotation
            return
        }
        selectedWord = WordAnnotation(word: cleaned, baseForm: cleaned, english: "Looking up...", partOfSpeech: "")
        Task {
            do {
                var annotation = try await WordLookup.lookup(word: cleaned, inContext: paragraph)
                annotation.source = "AI guess"
                lookupCache[cleaned] = annotation
                selectedWord = annotation
            } catch {
                selectedWord = WordAnnotation(word: cleaned, baseForm: cleaned, english: "Lookup unavailable", partOfSpeech: "")
            }
        }
    }

    private func handleSheetDismiss() {
        guard wasReadingBeforeTap else { return }
        wasReadingBeforeTap = false
        speech.resume()
    }

    private func cleanWord(_ word: String) -> String {
        word.lowercased()
            .trimmingCharacters(in: .punctuationCharacters)
            .trimmingCharacters(in: .whitespaces)
    }
}

// MARK: - Tappable paragraph

private struct TappableParagraph: View {
    let text: String
    let highlightedWordIndex: Int?
    let onTap: (String) -> Void

    var body: some View {
        let words = text.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
        FlowLayout(spacing: 0) {
            ForEach(Array(words.enumerated()), id: \.offset) { idx, word in
                WordButton(word: word, isHighlighted: idx == highlightedWordIndex, onTap: onTap)
            }
        }
        .accessibilityElement(children: .combine)
    }
}

private struct WordButton: View {
    let word: String
    let isHighlighted: Bool
    let onTap: (String) -> Void

    var body: some View {
        Button {
            onTap(word)
        } label: {
            Text(word + " ")
                .font(.body)
                .foregroundStyle(.primary)
                .padding(.horizontal, isHighlighted ? 2 : 0)
                .padding(.vertical, 1)
                .background(
                    isHighlighted
                        ? Color.yellow.opacity(0.35)
                        : Color.clear,
                    in: RoundedRectangle(cornerRadius: 4)
                )
                .animation(.easeInOut(duration: 0.15), value: isHighlighted)
        }
        .buttonStyle(.plain)
    }
}

// MARK: - Flow layout

private struct FlowLayout: Layout {
    var spacing: CGFloat = 0

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let rows = computeRows(proposal: proposal, subviews: subviews)
        var height: CGFloat = 0
        for row in rows {
            height += row.map { $0.height }.max() ?? 0
        }
        height += CGFloat(max(0, rows.count - 1)) * spacing
        return CGSize(width: proposal.width ?? 0, height: height)
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let rows = computeRows(proposal: proposal, subviews: subviews)
        var y = bounds.minY
        var subviewIndex = 0
        for row in rows {
            var x = bounds.minX
            let rowHeight = row.map { $0.height }.max() ?? 0
            for size in row {
                subviews[subviewIndex].place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size))
                x += size.width
                subviewIndex += 1
            }
            y += rowHeight + spacing
        }
    }

    private func computeRows(proposal: ProposedViewSize, subviews: Subviews) -> [[CGSize]] {
        let maxWidth = proposal.width ?? .infinity
        var rows: [[CGSize]] = [[]]
        var currentWidth: CGFloat = 0
        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            if currentWidth + size.width > maxWidth && !rows[rows.count - 1].isEmpty {
                rows.append([])
                currentWidth = 0
            }
            rows[rows.count - 1].append(size)
            currentWidth += size.width
        }
        return rows
    }
}

// MARK: - Word detail sheet

private struct WordDetailSheet: View {
    let word: WordAnnotation

    var body: some View {
        VStack(spacing: 16) {
            HStack {
                Text(word.word)
                    .font(.title2.bold())
                Spacer()
                if !word.partOfSpeech.isEmpty {
                    Text(word.partOfSpeech)
                        .font(.caption.weight(.medium))
                        .foregroundStyle(.secondary)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                        .background(.fill.tertiary, in: Capsule())
                }
            }

            Divider()

            if word.english == "Looking up..." {
                HStack(spacing: 8) {
                    ProgressView()
                    Text("Looking up word...")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity)
            } else {
                VStack(alignment: .leading, spacing: 8) {
                    if !word.baseForm.isEmpty && word.baseForm != word.word {
                        HStack {
                            Text("Base form:")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                            Text(word.baseForm)
                                .font(.subheadline.weight(.semibold))
                                .italic()
                        }
                    }
                    if !word.english.isEmpty {
                        HStack {
                            Text("English:")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                            Text(word.english)
                                .font(.subheadline.weight(.semibold))
                        }
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }

            Spacer()

            if !word.source.isEmpty {
                Text(sourceLabel)
                    .font(.caption2)
                    .foregroundStyle(.tertiary)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .padding()
    }

    private var sourceLabel: String {
        word.source == "AI guess"
            ? "AI guess · on-device estimate, may be approximate"
            : "Source: \(word.source)"
    }
}

// MARK: - On-demand word lookup (matches StoryReaderView's WordLookup)

@MainActor
private enum WordLookup {
    @Generable
    struct WordInfo {
        @Guide(description: "The dictionary base form (infinitive for verbs, singular for nouns)")
        var baseForm: String
        @Guide(description: "English translation")
        var english: String
        @Guide(description: "Part of speech: verb, noun, adjective, adverb, preposition, conjunction, article, pronoun, or other")
        var partOfSpeech: String
    }

    static func lookup(word: String, inContext sentence: String) async throws -> WordAnnotation {
        let session = LanguageModelSession(instructions: """
            You are a Spanish dictionary. Given a word and the sentence it appears in, \
            provide its base form, English translation, and part of speech.
            """)
        let response = try await session.respond(
            to: "Word: \"\(word)\" in sentence: \"\(sentence)\"",
            generating: WordInfo.self
        )
        let info = response.content
        return WordAnnotation(
            word: word,
            baseForm: info.baseForm,
            english: info.english,
            partOfSpeech: info.partOfSpeech
        )
    }
}
