import AVFoundation
import Foundation
import PDFKit
import SwiftUI
import UIKit

@MainActor
final class StudyChatController: ObservableObject {
    let sessionId: String
    let scenario: StudyAskScenario

    @Published var messages: [ChatMessage] = []
    @Published var question = ""
    @Published var busy = false
    @Published var error: String?
    @Published var threadSummaries: [ChatThreadSummary] = []
    @Published var activeThreadId = ""
    @Published var attachmentImage: UIImage?
    @Published var attachmentAudio: (start: Double, end: Double)?
    @Published var speakingMessageId: String?
    @Published var ttsHint: String?
    @Published var featureDisabled = false
    @Published var historyReady = false

    private let persister = ChatHistoryPersister()
    private var askTask: Task<Void, Never>?
    private var runtimeConfig: AiRuntimeConfig?

    init(sessionId: String, scenario: StudyAskScenario) {
        self.sessionId = sessionId
        self.scenario = scenario
    }

    var canSend: Bool {
        !busy && !featureDisabled && (
            !question.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                || attachmentImage != nil
                || attachmentAudio != nil
        )
    }

    var lastAssistantMessage: ChatMessage? {
        messages.last(where: { $0.role == .assistant && !$0.text.isEmpty })
    }

    func loadHistory() {
        persister.hydrate(sessionId: sessionId)
        guard let file = persister.currentFile() else {
            historyReady = false
            return
        }
        let active = ChatHistoryHelper.activeThread(from: file)
        messages = active.messages
        activeThreadId = active.threadId
        threadSummaries = ChatHistoryStore.threadSummaries(from: file)
        historyReady = persister.isPersistEnabled
        Task { await refreshFeatureGate() }
    }

    func flush() {
        persister.flush()
    }

    func stopSpeaking() {
        TtsClient.stopSpeaking()
        speakingMessageId = nil
    }

    func refreshFeatureGate() async {
        do {
            let cfg = try await AiConfigClient.resolveConfig { [scenario] c in
                scenario.featureGate(c.features)
            }
            runtimeConfig = cfg
            featureDisabled = false
        } catch AiConfigResolveError.featureDisabled {
            featureDisabled = true
            error = AiConfigResolveError.featureDisabled.errorDescription
        } catch {
            featureDisabled = false
        }
    }

    func send() {
        let text = question.trimmingCharacters(in: .whitespacesAndNewlines)
        guard canSend else { return }
        askTask?.cancel()

        let displayText = text.isEmpty ? "(片段/截图提问)" : text
        var apiUserText = text.isEmpty ? "请解释当前画面/片段。" : text
        if let clip = attachmentAudio {
            apiUserText += String(format: "\n\n时间范围: %.2fs – %.2fs", clip.start, clip.end)
        }

        var imagePath: String?
        var imageB64: String?
        if let img = attachmentImage, let data = img.jpegData(compressionQuality: 0.85) {
            imageB64 = data.base64EncodedString()
            apiUserText += "\n\n(已附带截图)"
            imagePath = saveAttachmentImage(data)
        }

        let userMsg = ChatMessage.userText(displayText, imagePath: imagePath)
        messages.append(userMsg)
        question = ""
        attachmentImage = nil
        attachmentAudio = nil
        persistMessages()

        streamAssistantReply(apiUserText: apiUserText, imageB64: imageB64)
    }

    func sendVoice(audioPath: String, durationSec: Double, transcript: String) {
        guard !busy, !featureDisabled else { return }
        askTask?.cancel()

        let displayTranscript = transcript.trimmingCharacters(in: .whitespacesAndNewlines)
        let spokenText = displayTranscript.isEmpty ? "（语音消息）" : displayTranscript
        var apiUserText = displayTranscript.isEmpty
            ? "这是一条语音提问，请结合当前画面/片段回答。"
            : displayTranscript

        var imagePath: String?
        var imageB64: String?
        if let img = attachmentImage, let data = img.jpegData(compressionQuality: 0.85) {
            imageB64 = data.base64EncodedString()
            apiUserText += "\n\n(已附带截图)"
            imagePath = saveAttachmentImage(data)
        }
        if let clip = attachmentAudio {
            apiUserText += String(format: "\n\n时间范围: %.2fs – %.2fs", clip.start, clip.end)
        }

        let userMsg = ChatMessage.userVoice(
            transcript: spokenText,
            audioPath: audioPath,
            durationSec: durationSec,
            imagePath: imagePath
        )
        messages.append(userMsg)
        attachmentImage = nil
        attachmentAudio = nil
        persistMessages()

        streamAssistantReply(apiUserText: apiUserText, imageB64: imageB64)
    }

    private func streamAssistantReply(apiUserText: String, imageB64: String?) {
        let assistantId = "msg_\(Int(Date().timeIntervalSince1970 * 1_000_000))"
        messages.append(ChatMessage(id: assistantId, role: .assistant, kind: .text, text: ""))
        busy = true
        error = nil

        askTask = Task {
            defer { busy = false }
            do {
                let cfg = try await AiConfigClient.resolveConfig { [scenario] c in
                    scenario.featureGate(c.features)
                }
                runtimeConfig = cfg
                let system = scenario.systemPrompt(cfg.prompts)
                let prior = ChatHistoryHelper.buildHistoryTurns(from: Array(messages.dropLast(2)))
                let history = prior.map { AiChatClient.HistoryMessage(role: $0.role.rawValue, text: $0.text) }
                let capability = imageB64 == nil ? "chat" : "vision"

                try await AiChatClient.stream(
                    config: cfg,
                    system: system,
                    user: apiUserText,
                    imageJPEGBase64: imageB64,
                    history: history,
                    capability: capability
                ) { [weak self] delta in
                    Task { @MainActor in
                        guard let self else { return }
                        if let idx = self.messages.firstIndex(where: { $0.id == assistantId }) {
                            self.messages[idx].text = delta
                        }
                    }
                }

                if Task.isCancelled { return }
                persistMessages()
            } catch is CancellationError {
                if let idx = messages.firstIndex(where: { $0.id == assistantId }), messages[idx].text.isEmpty {
                    messages.remove(at: idx)
                }
            } catch AiConfigResolveError.featureDisabled {
                error = AiConfigResolveError.featureDisabled.errorDescription
                messages.removeAll { $0.id == assistantId }
            } catch let err {
                self.error = err.localizedDescription
                if let idx = messages.firstIndex(where: { $0.id == assistantId }) {
                    messages[idx].text = "（请求失败）"
                }
                persistMessages()
            }
        }
    }

    func speak(message: ChatMessage) {
        guard message.role == .assistant, !message.text.isEmpty else { return }
        speakingMessageId = message.id
        ttsHint = nil
        Task { @MainActor in
            defer { speakingMessageId = nil }
            do {
                var cfg = runtimeConfig
                if cfg == nil {
                    cfg = try? await AiConfigClient.resolveConfig { _ in true }
                }
                let result = try await TtsClient.speakText(message.text, config: cfg)
                ttsHint = result.fallbackHint
            } catch {
                ttsHint = error.localizedDescription
            }
        }
    }

    func attachPastedImage(_ image: UIImage) {
        attachmentImage = image
    }

    func attachVideoFrame(from player: AVPlayer?) {
        guard let player, let item = player.currentItem else { return }
        let time = player.currentTime()
        let generator = AVAssetImageGenerator(asset: item.asset)
        generator.appliesPreferredTrackTransform = true
        generator.requestedTimeToleranceBefore = .zero
        generator.requestedTimeToleranceAfter = .zero
        if let cg = try? generator.copyCGImage(at: time, actualTime: nil) {
            attachmentImage = UIImage(cgImage: cg)
        }
    }

    func attachPdfPage(from document: PDFDocument?, page: Int) {
        guard let document else { return }
        let idx = max(0, min(page - 1, document.pageCount - 1))
        guard let pdfPage = document.page(at: idx) else { return }
        let bounds = pdfPage.bounds(for: .mediaBox)
        let scale: CGFloat = 2
        let size = CGSize(width: bounds.width * scale, height: bounds.height * scale)
        let renderer = UIGraphicsImageRenderer(size: size)
        attachmentImage = renderer.image { ctx in
            UIColor.white.setFill()
            ctx.fill(CGRect(origin: .zero, size: size))
            ctx.cgContext.translateBy(x: 0, y: size.height)
            ctx.cgContext.scaleBy(x: scale, y: -scale)
            pdfPage.draw(with: .mediaBox, to: ctx.cgContext)
        }
    }

    func setAudioClip(start: Double, end: Double) {
        attachmentAudio = (min(start, end), max(start, end))
    }

    func selectThread(_ threadId: String) {
        guard threadId != activeThreadId, var file = persister.currentFile() else { return }
        persister.flush()
        guard let thread = file.threads.first(where: { $0.id == threadId }) else { return }
        file.activeThreadId = threadId
        persister.replaceFile(file, skipPersist: true)
        activeThreadId = threadId
        messages = ChatHistoryHelper.normalizeMessages(thread.messages)
        threadSummaries = ChatHistoryStore.threadSummaries(from: file)
    }

    func newThread() {
        persister.flush()
        guard var file = persister.currentFile() else { return }
        let id = ChatHistoryStore.newThreadId()
        let now = ChatHistoryStore.isoNow()
        file.threads.insert(ChatThread(id: id, title: nil, updatedAt: now, messages: []), at: 0)
        file.activeThreadId = id
        try? ChatHistoryStore.save(file)
        persister.replaceFile(file, skipPersist: true)
        activeThreadId = id
        messages = []
        threadSummaries = ChatHistoryStore.threadSummaries(from: file)
    }

    func clearThread() {
        stopSpeaking()
        askTask?.cancel()
        messages = []
        persistMessages()
    }

    private func persistMessages() {
        persister.scheduleSave(messages: messages, activeThreadId: activeThreadId)
        if let file = persister.currentFile() {
            threadSummaries = ChatHistoryStore.threadSummaries(from: file)
        }
    }

    private func saveAttachmentImage(_ data: Data) -> String? {
        let dir = ChatHistoryStore.rootURL.appendingPathComponent("attachments", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        let name = "img_\(Int(Date().timeIntervalSince1970 * 1_000)).jpg"
        let url = dir.appendingPathComponent(name)
        guard (try? data.write(to: url)) != nil else { return nil }
        return ChatHistoryHelper.relativeAttachmentPath(for: url)
    }
}
