import SwiftUI
import UIKit

/// Full-screen content viewer for tool output (UIKit).
///
/// Supports code (with syntax highlighting), diff, and markdown modes.
/// Presented via ``FullScreenCodeView`` (UIViewControllerRepresentable wrapper)
/// from SwiftUI callers, and directly from UIKit timeline cells.
///
/// Supports two presentation modes:
/// - `.sheet`: standalone presentation with its own dismiss button (chevron.down).
///   Used by timeline full-screen and SwiftUI `.sheet`/`.fullScreenCover`.
/// - `.embedded(onDismiss:)`: embedded in a SwiftUI NavigationStack. Shows a
///   back button (chevron.backward) that calls the provided closure.

final class FullScreenCodeViewController: UIViewController {

    /// Controls how the viewer presents its dismiss affordance.
    enum PresentationMode {
        /// Standalone sheet/fullScreenCover — chevron.down + `dismiss(animated:)`.
        case sheet
        /// Embedded inside a SwiftUI NavigationStack — chevron.backward + closure.
        case embedded(onDismiss: @MainActor @Sendable () -> Void)
    }

    private struct Presentation {
        let bodyContent: FullScreenCodeContent
        let copyText: String
        let sourceToggleTitle: String?
    }

    private struct NavigationPresentation: Equatable {
        let sourceToggleTitle: String?

        init(_ presentation: Presentation) {
            sourceToggleTitle = presentation.sourceToggleTitle
        }
    }

    private let content: FullScreenCodeContent
    private let presentationMode: PresentationMode
    private let selectedTextPiRouter: SelectedTextPiActionRouter?
    private let selectedTextSessionId: String?
    private let selectedTextSourceLabel: String?
    private var showSource = false
    private var copyButton: UIBarButtonItem?
    private weak var contentHostController: UIViewController?
    private var installedBodyView: UIView?
    private var liveSourceBodyView: NativeFullScreenSourceBody?
    private var liveSourceObserverID: UUID?
    private var liveSourceCurrentSnapshot: SourceTraceStream.Snapshot?
    private var lastNavigationPresentation: NavigationPresentation?

    init(
        content: FullScreenCodeContent,
        presentationMode: PresentationMode = .sheet,
        selectedTextPiRouter: SelectedTextPiActionRouter? = nil,
        selectedTextSessionId: String? = nil,
        selectedTextSourceLabel: String? = nil
    ) {
        self.content = content
        self.presentationMode = presentationMode
        self.selectedTextPiRouter = selectedTextPiRouter
        self.selectedTextSessionId = selectedTextSessionId
        self.selectedTextSourceLabel = selectedTextSourceLabel
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { nil }

    /// Present the code viewer from the topmost view controller.
    /// Works from both UIKit and SwiftUI contexts without needing a
    /// responder-chain walk from a specific source view.
    static func present(
        content: FullScreenCodeContent,
        selectedTextPiRouter: SelectedTextPiActionRouter? = nil,
        selectedTextSessionId: String? = nil,
        selectedTextSourceLabel: String? = nil
    ) {
        guard let scene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene }).first,
              let root = scene.windows.first(where: \.isKeyWindow)?.rootViewController else { return }
        var presenter = root
        while let presented = presenter.presentedViewController {
            presenter = presented
        }
        // Don't stack on top of an existing fullscreen viewer.
        if presenter is FullScreenCodeViewController
            || presenter is FullScreenImageViewController { return }

        let controller = FullScreenCodeViewController(
            content: content,
            selectedTextPiRouter: selectedTextPiRouter,
            selectedTextSessionId: selectedTextSessionId,
            selectedTextSourceLabel: selectedTextSourceLabel
        )
        controller.modalPresentationStyle = .pageSheet
        if let sheet = controller.sheetPresentationController {
            sheet.detents = [.large()]
            sheet.prefersGrabberVisible = true
        }
        controller.overrideUserInterfaceStyle = ThemeRuntimeState.currentThemeID()
            .preferredColorScheme == .light ? .light : .dark
        presenter.present(controller, animated: true)
    }

    deinit {
        if let liveSourceObserverID,
           case .liveSource(_, let stream) = content {
            Task { @MainActor in
                stream.removeObserver(liveSourceObserverID)
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let palette = ThemeRuntimeState.currentThemeID().palette
        view.backgroundColor = UIColor(palette.bgDark)

        let nav = UINavigationController(rootViewController: makeContentController())
        // Disable internal interactive pop — this nav only has one root VC.
        // Prevents conflict with the hosting SwiftUI navigation's swipe-back
        // when in embedded mode.
        nav.interactivePopGestureRecognizer?.isEnabled = false
        nav.view.translatesAutoresizingMaskIntoConstraints = false
        addChild(nav)
        view.addSubview(nav.view)
        NSLayoutConstraint.activate([
            nav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            nav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            nav.view.topAnchor.constraint(equalTo: view.topAnchor),
            nav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        nav.didMove(toParent: self)
    }

    private func makeContentController() -> UIViewController {
        let palette = ThemeRuntimeState.currentThemeID().palette
        let vc = UIViewController()
        vc.view.backgroundColor = UIColor(palette.bgDark)

        let doneIcon: String
        switch presentationMode {
        case .sheet:
            doneIcon = "chevron.down"
        case .embedded:
            doneIcon = "chevron.backward"
        }
        let doneButton = UIBarButtonItem(
            image: UIImage(systemName: doneIcon),
            style: .plain,
            target: self,
            action: #selector(doneTapped)
        )
        doneButton.tintColor = UIColor(palette.cyan)
        vc.navigationItem.leftBarButtonItem = doneButton

        contentHostController = vc

        // No custom UINavigationBarAppearance — iOS 26 Liquid Glass renders
        // bar items as floating glass pills. See FullScreenViewerChrome.

        installInitialBody(on: vc, palette: palette)
        configureNavigation(on: vc, palette: palette)

        return vc
    }

    private func installInitialBody(on viewController: UIViewController, palette: ThemePalette) {
        switch content {
        case .liveSource(let snapshot, let stream):
            liveSourceCurrentSnapshot = snapshot
            let body = makeLiveSourceBody(snapshot: snapshot, palette: palette)
            installBodyView(body, on: viewController)
            liveSourceObserverID = stream.addObserver(deliverImmediately: false) { [weak self] snapshot in
                self?.handleLiveSourceUpdate(snapshot)
            }

        default:
            let presentation = makePresentation()
            installBodyView(makeBodyView(for: presentation.bodyContent, palette: palette), on: viewController)
        }
    }

    private func installBodyView(_ bodyView: UIView, on viewController: UIViewController) {
        installedBodyView?.removeFromSuperview()
        installedBodyView = bodyView
        bodyView.translatesAutoresizingMaskIntoConstraints = false
        viewController.view.addSubview(bodyView)
        // Top pinned to view edge (not safe area) so content scrolls behind
        // the navigation bar's Liquid Glass pills. See FullScreenViewerChrome.
        NSLayoutConstraint.activate([
            bodyView.leadingAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.leadingAnchor),
            bodyView.trailingAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.trailingAnchor),
            bodyView.topAnchor.constraint(equalTo: viewController.view.topAnchor),
            bodyView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor),
        ])
    }

    private func configureNavigation(on viewController: UIViewController, palette: ThemePalette) {
        let presentation = makePresentation()
        let navigationPresentation = NavigationPresentation(presentation)
        guard navigationPresentation != lastNavigationPresentation else {
            return
        }

        lastNavigationPresentation = navigationPresentation
        // No titleView — immersive mode shows only floating glass pills.
        // See FullScreenViewerChrome.

        var rightItems: [UIBarButtonItem] = []
        let copy = UIBarButtonItem(
            image: UIImage(systemName: "doc.on.doc"),
            style: .plain,
            target: self,
            action: #selector(copyTapped)
        )
        copy.tintColor = UIColor(palette.fgDim)
        copyButton = copy
        rightItems.append(copy)

        if let shareable = shareableContent() {
            rightItems.append(
                FileSharePresenter.makeShareBarButtonItem(
                    for: shareable,
                    tintColor: UIColor(palette.fgDim)
                )
            )
        }

        if let toggleTitle = presentation.sourceToggleTitle {
            let toggle = UIBarButtonItem(
                title: toggleTitle,
                style: .plain,
                target: self,
                action: #selector(toggleSource)
            )
            toggle.tintColor = UIColor(palette.blue)
            rightItems.append(toggle)
        }

        viewController.navigationItem.rightBarButtonItems = rightItems
    }

    private func makePresentation() -> Presentation {
        let semanticContent = currentSemanticContent()
        return Presentation(
            bodyContent: bodyContent(for: semanticContent),
            copyText: copyText(for: semanticContent),
            sourceToggleTitle: sourceToggleTitle(for: semanticContent)
        )
    }

    // MARK: - Body

    private func makeBodyView(for content: FullScreenCodeContent, palette: ThemePalette) -> UIView {
        switch content {
        case .code(let text, let language, let filePath, let startLine):
            return NativeFullScreenCodeBody(
                content: text,
                language: language,
                startLine: startLine,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenCode,
                    filePath: filePath,
                    languageHint: language
                )
            )
        case .plainText(let text, let filePath):
            return NativeFullScreenSourceBody(
                content: text,
                isStreaming: false,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenSource,
                    filePath: filePath
                )
            )
        case .diff(let oldText, let newText, let filePath, let precomputedLines):
            return NativeFullScreenDiffBody(
                oldText: oldText,
                newText: newText,
                filePath: filePath,
                precomputedLines: precomputedLines,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenDiff,
                    filePath: filePath
                )
            )
        case .markdown(let text, let filePath, let wsContext):
            let body = NativeFullScreenMarkdownBody(
                content: text,
                stream: nil,
                palette: palette,
                plainTextFallbackThreshold: nil,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenMarkdown,
                    filePath: filePath
                ),
                workspaceID: wsContext?.workspaceID,
                serverBaseURL: wsContext?.serverBaseURL,
                sourceFilePath: filePath,
                perfSurface: .fullScreenMarkdown,
                fetchWorkspaceFile: wsContext?.fetchWorkspaceFile
            )
            return body
        case .html(let text, let filePath):
            let piHandler = makePiWebViewHandler(
                router: selectedTextPiRouter,
                sourceContext: makeSourceContext(surface: .fullScreenSource, filePath: filePath)
            )
            let view = HTMLRenderView(htmlString: text, piActionHandler: piHandler)
            view.backgroundColor = UIColor(palette.bgDark)
            return view
        case .thinking(let text, let stream):
            return NativeFullScreenMarkdownBody(
                content: text,
                stream: stream,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenThinking,
                    fallbackSourceLabel: String(localized: "Thinking")
                ),
                perfSurface: .fullScreenThinking
            )
        case .terminal(let text, let command, let stream):
            return NativeFullScreenTerminalBody(
                content: text,
                command: command,
                stream: stream,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenTerminal,
                    fallbackSourceLabel: command
                )
            )
        case .liveSource(let snapshot, _):
            return makeBodyView(for: bodyContent(for: snapshot), palette: palette)

        // Document renderers — use rendered views with source toggle
        case .latex(let text, let filePath):
            return NativeFullScreenRenderedDocumentBody(
                content: .latex(text),
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenCode,
                    filePath: filePath,
                    languageHint: "latex"
                )
            )
        case .orgMode(let text, let filePath):
            return NativeFullScreenRenderedDocumentBody(
                content: .orgMode(text),
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenCode,
                    filePath: filePath,
                    languageHint: "org"
                )
            )
        case .mermaid(let text, let filePath):
            return NativeFullScreenRenderedDocumentBody(
                content: .mermaid(text),
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenCode,
                    filePath: filePath,
                    languageHint: "mermaid"
                )
            )
        case .graphviz(let text, let filePath):
            return NativeFullScreenCodeBody(
                content: text,
                language: "dot",
                startLine: 1,
                palette: palette,
                selectedTextPiRouter: selectedTextPiRouter,
                selectedTextSourceContext: makeSourceContext(
                    surface: .fullScreenCode,
                    filePath: filePath,
                    languageHint: "dot"
                )
            )
        }
    }

    private func makeLiveSourceBody(
        snapshot: SourceTraceStream.Snapshot,
        palette: ThemePalette
    ) -> NativeFullScreenSourceBody {
        let body = NativeFullScreenSourceBody(
            content: snapshot.text,
            isStreaming: !snapshot.isDone,
            palette: palette,
            selectedTextPiRouter: selectedTextPiRouter,
            selectedTextSourceContext: makeSourceContext(
                surface: .fullScreenSource,
                filePath: snapshot.filePath
            )
        )
        liveSourceBodyView = body
        return body
    }

    private func handleLiveSourceUpdate(_ snapshot: SourceTraceStream.Snapshot) {
        liveSourceCurrentSnapshot = snapshot
        guard let viewController = contentHostController else { return }

        let palette = ThemeRuntimeState.currentThemeID().palette
        if snapshot.isDone {
            liveSourceBodyView = nil
            let presentation = makePresentation()
            installBodyView(makeBodyView(for: presentation.bodyContent, palette: palette), on: viewController)
        } else if let liveSourceBodyView {
            liveSourceBodyView.update(content: snapshot.text, isStreaming: true)
        } else {
            installBodyView(makeLiveSourceBody(snapshot: snapshot, palette: palette), on: viewController)
        }

        configureNavigation(on: viewController, palette: palette)
    }

    private func currentSemanticContent() -> FullScreenCodeContent {
        switch content {
        case .liveSource(let snapshot, _):
            return semanticContent(for: liveSourceCurrentSnapshot ?? snapshot)
        default:
            return content
        }
    }

    private func semanticContent(for snapshot: SourceTraceStream.Snapshot) -> FullScreenCodeContent {
        if snapshot.isDone,
           let finalContent = snapshot.finalContent {
            return finalContent
        }
        return .plainText(content: snapshot.text, filePath: snapshot.filePath)
    }

    private func bodyContent(for snapshot: SourceTraceStream.Snapshot) -> FullScreenCodeContent {
        bodyContent(for: semanticContent(for: snapshot))
    }

    private func bodyContent(for content: FullScreenCodeContent) -> FullScreenCodeContent {
        if showSource {
            if case .markdown(let text, let filePath, _) = content {
                return .plainText(content: text, filePath: filePath)
            }
            if case .html(let text, let filePath) = content {
                return .code(content: text, language: "html", filePath: filePath, startLine: 1)
            }
            if case .diff(_, _, let filePath, let precomputedLines) = content,
               Self.isHTMLFilePath(filePath),
               let lines = precomputedLines {
                let fullNewText = lines
                    .filter { $0.kind != .removed }
                    .map(\.text)
                    .joined(separator: "\n")
                return .html(content: fullNewText, filePath: filePath)
            }
            // Document types: toggle to source view
            if case .latex(let text, let filePath) = content {
                return .code(content: text, language: "latex", filePath: filePath, startLine: 1)
            }
            if case .orgMode(let text, let filePath) = content {
                return .code(content: text, language: "org", filePath: filePath, startLine: 1)
            }
            if case .mermaid(let text, let filePath) = content {
                return .code(content: text, language: "mermaid", filePath: filePath, startLine: 1)
            }
        }
        return content
    }

    private func sourceToggleTitle(for content: FullScreenCodeContent) -> String? {
        switch content {
        case .markdown:
            return showSource ? String(localized: "Reader") : String(localized: "Source")
        case .html:
            return showSource ? String(localized: "Preview") : String(localized: "Source")
        case .diff(_, _, let filePath, let precomputedLines):
            guard Self.isHTMLFilePath(filePath), precomputedLines != nil else { return nil }
            return showSource ? String(localized: "Diff") : String(localized: "Render")
        case .latex, .orgMode, .mermaid:
            return showSource ? String(localized: "Rendered") : String(localized: "Source")
        default:
            return nil
        }
    }

    // MARK: - HTML Diff Helpers

    private static func isHTMLFilePath(_ filePath: String?) -> Bool {
        guard let filePath else { return false }
        let ext = (filePath as NSString).pathExtension.lowercased()
        return ext == "html" || ext == "htm"
    }

    // MARK: - Actions

    @objc private func doneTapped() {
        switch presentationMode {
        case .sheet:
            dismiss(animated: true)
        case .embedded(let onDismiss):
            onDismiss()
        }
    }

    @objc private func copyTapped() {
        UIPasteboard.general.string = makePresentation().copyText
        copyButton?.image = UIImage(systemName: "checkmark")
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.copyButton?.image = UIImage(systemName: "doc.on.doc")
        }
    }

    private func copyText(for content: FullScreenCodeContent) -> String {
        switch content {
        case .code(let text, _, _, _):
            return text
        case .plainText(let text, _):
            return text
        case .diff(_, let newText, _, _):
            return newText
        case .markdown(let text, _, _):
            return text
        case .html(let text, _):
            return text
        case .thinking(let text, let stream):
            return stream?.snapshot.text ?? text
        case .terminal(let text, _, let stream):
            return stream?.snapshot.output ?? text
        case .liveSource(let snapshot, _):
            return copyText(for: semanticContent(for: snapshot))
        case .latex(let text, _), .orgMode(let text, _),
             .mermaid(let text, _), .graphviz(let text, _):
            return text
        }
    }

    private func makeSourceContext(
        surface: SelectedTextSurfaceKind,
        filePath: String? = nil,
        languageHint: String? = nil,
        fallbackSourceLabel: String? = nil
    ) -> SelectedTextSourceContext? {
        // Allow empty session ID for file browser context where there's no
        // active session but we still want the π quick action menu.
        guard selectedTextPiRouter != nil else { return nil }
        return SelectedTextSourceContext(
            sessionId: selectedTextSessionId ?? "",
            surface: surface,
            sourceLabel: selectedTextSourceLabel ?? fallbackSourceLabel,
            filePath: filePath,
            languageHint: languageHint
        )
    }

    /// Bridge a pi router + source context into a closure for HTMLRenderView.
    private func makePiWebViewHandler(
        router: SelectedTextPiActionRouter?,
        sourceContext: SelectedTextSourceContext?
    ) -> ((String, PiQuickAction) -> Void)? {
        guard let router, let sourceContext else { return nil }
        return { text, quickAction in
            router.dispatch(SelectedTextPiRequest(
                action: quickAction,
                selectedText: text,
                source: sourceContext
            ))
        }
    }

    // MARK: - Share

    private func shareableContent() -> FileShareService.ShareableContent? {
        let content = currentSemanticContent()
        switch content {
        case .mermaid(let text, _): return .mermaid(text)
        case .latex(let text, _): return .latex(text)
        case .markdown(let text, _, _): return .markdown(text)
        case .orgMode(let text, _): return .orgMode(text)
        case .html(let text, _): return .html(text)
        case .graphviz(let text, _): return .code(text, language: "dot")
        case .code(let text, let lang, _, _): return .code(text, language: lang)
        case .plainText(let text, _): return .plainText(text)
        case .thinking(let text, let stream):
            return .plainText(stream?.snapshot.text ?? text)
        case .terminal(let text, _, let stream):
            return .plainText(stream?.snapshot.output ?? text)
        case .diff(_, let newText, _, _): return .plainText(newText)
        case .liveSource(let snapshot, _):
            return .plainText(snapshot.text)
        }
    }

    @objc private func toggleSource() {
        guard makePresentation().sourceToggleTitle != nil,
              let viewController = contentHostController else {
            return
        }

        showSource.toggle()
        let palette = ThemeRuntimeState.currentThemeID().palette
        let presentation = makePresentation()
        installBodyView(makeBodyView(for: presentation.bodyContent, palette: palette), on: viewController)
        configureNavigation(on: viewController, palette: palette)
    }
}
