import SwiftUI
import NucleicCore

// MARK: - Build/Run controls + console

extension BuildRunCommand.Kind {
    var title: String { self == .build ? "Build" : "Run" }
    var systemImage: String { self == .build ? "hammer" : "play.fill" }
}

/// Build / Run buttons. Detects candidate commands from the manifests in the working
/// directory (Makefile, package.json, Package.swift, …) and runs the chosen one on the
/// host, streaming output into a console sheet. Renders nothing for a kind with no
/// detected commands; a single command is a plain button, several become a menu.
///
/// Used in two places: the project header (`compact == false`, bordered buttons over the
/// project root) and the open session's toolbar (`compact == true`, toolbar-styled buttons
/// over the session's worktree).
struct BuildRunControls: View {
    let project: Project
    /// Where to detect manifests and run commands. Defaults to the project root; a session
    /// passes its worktree path so build/run act on that chat's working copy.
    var workingDirectory: String? = nil
    /// Toolbar styling: drop the explicit `.bordered` chrome so the buttons adopt the
    /// surrounding toolbar's look.
    var compact = false

    @State private var commands: [BuildRunCommand] = []
    @State private var running: BuildRunCommand?

    private var cwd: String { workingDirectory ?? project.rootPath }

    var body: some View {
        Group {
            control(for: .build)
            control(for: .run)
        }
        .task(id: cwd) {
            let path = cwd
            commands = await Task.detached { BuildRunDetector.detect(in: path) }.value
        }
        .sheet(item: $running) { command in
            BuildRunConsoleView(command: command, cwd: cwd)
        }
    }

    @ViewBuilder
    private func control(for kind: BuildRunCommand.Kind) -> some View {
        let items = commands.filter { $0.kind == kind }
        if items.count == 1 {
            applyStyle(
                Button { running = items[0] } label: {
                    Label(kind.title, systemImage: kind.systemImage)
                }
                .help(items[0].label))
        } else if items.count > 1 {
            applyStyle(
                Menu {
                    ForEach(items) { command in
                        Button(command.label) { running = command }
                    }
                } label: {
                    Label(kind.title, systemImage: kind.systemImage)
                }
                .menuStyle(.button))
        }
    }

    /// In the header, give the control a bordered look and intrinsic width; in a toolbar,
    /// leave it bare so it matches the neighboring toolbar items.
    @ViewBuilder
    private func applyStyle(_ control: some View) -> some View {
        if compact {
            control
        } else {
            control.buttonStyle(.bordered).fixedSize()
        }
    }
}

/// Live console for a single build/run command: streams stdout/stderr as it arrives,
/// reports the exit status, and lets the user stop the process. Closing the sheet
/// terminates a still-running command.
struct BuildRunConsoleView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.appPalette) private var palette

    let command: BuildRunCommand
    let cwd: String

    @State private var runner = BuildRunRunner()

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            header
            Divider()
            console
            Divider()
            footer
        }
        .frame(width: 660, height: 460)
        .task { runner.start(command: command.command, cwd: cwd) }
        .onDisappear { runner.stop() }
    }

    private var header: some View {
        HStack(spacing: 10) {
            Image(systemName: command.kind.systemImage)
                .foregroundStyle(.tint)
            VStack(alignment: .leading, spacing: 1) {
                Text(command.label).font(.headline)
                Text("\(command.source) · \(cwd)")
                    .font(.caption).foregroundStyle(.secondary)
                    .lineLimit(1).truncationMode(.middle)
            }
            Spacer()
            statusBadge
        }
        .padding(14)
    }

    @ViewBuilder
    private var statusBadge: some View {
        switch runner.phase {
        case .idle, .running:
            HStack(spacing: 6) {
                ProgressView().controlSize(.small)
                Text("Running").font(.caption).foregroundStyle(.secondary)
            }
        case .finished(let code):
            Label(code == 0 ? "Succeeded" : "Exited \(code)",
                  systemImage: code == 0 ? "checkmark.circle.fill" : "xmark.octagon.fill")
                .font(.caption.weight(.medium))
                .foregroundStyle(code == 0 ? palette.success : palette.danger)
        case .failed(let message):
            Label(message, systemImage: "exclamationmark.triangle.fill")
                .font(.caption.weight(.medium))
                .foregroundStyle(palette.danger)
                .lineLimit(1)
        }
    }

    private var console: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 0) {
                    ForEach(runner.lines) { line in
                        Text(line.text.isEmpty ? " " : line.text)
                            .font(.system(.caption, design: .monospaced))
                            .foregroundStyle(line.isError ? palette.danger : AppTheme.primaryText)
                            .textSelection(.enabled)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .id(line.id)
                    }
                    Color.clear.frame(height: 1).id(consoleBottomID)
                }
                .padding(12)
            }
            .background(AppTheme.background)
            .onChange(of: runner.lines.count) {
                withAnimation(.linear(duration: 0.1)) { proxy.scrollTo(consoleBottomID, anchor: .bottom) }
            }
        }
    }

    private var footer: some View {
        HStack {
            Text(command.command)
                .font(.system(.caption2, design: .monospaced))
                .foregroundStyle(.secondary)
                .lineLimit(1).truncationMode(.middle)
            Spacer()
            if runner.isRunning {
                Button("Stop") { runner.stop() }
            }
            Button("Close") { dismiss() }
                .keyboardShortcut(.defaultAction)
        }
        .padding(14)
    }

    private let consoleBottomID = "build-run-console-bottom"
}

/// Drives one host command, accumulating its output lines on the main actor for the
/// console view. Output is line-buffered by `ProcessHost`; we keep the last `maxLines`
/// so a runaway log can't grow without bound.
@MainActor
@Observable
final class BuildRunRunner {
    enum Phase: Equatable {
        case idle
        case running
        case finished(Int32)
        case failed(String)
    }

    struct ConsoleLine: Identifiable {
        let id: Int
        let text: String
        let isError: Bool
    }

    private(set) var lines: [ConsoleLine] = []
    private(set) var phase: Phase = .idle

    var isRunning: Bool { phase == .running || phase == .idle }

    private let host = ProcessHost()
    private var handle: (any ProcessHandle)?
    private var task: Task<Void, Never>?
    private var started = false
    private var nextID = 0
    private let maxLines = 5_000

    func start(command: String, cwd: String) {
        guard !started else { return }
        started = true
        phase = .running
        task = Task { await run(command: command, cwd: cwd) }
    }

    /// Signal the running process to stop (SIGINT first; the process group winds down).
    func stop() {
        handle?.sendSignal(SIGINT)
    }

    private func run(command: String, cwd: String) async {
        // Pin this host build/run's git (every remote) to the Managed Git key (never the user's
        // ssh-agent/personal keys). GIT_SSH_COMMAND governs only git, so a standalone `ssh` the
        // build runs is unaffected. No-op when no Managed Git credential is configured. Mirrors
        // host_exec (`runOnHost`).
        let (gitEnv, gitCleanup) = GitHubCredentialProvisioner.provisionHost()
        defer { gitCleanup() }
        var env = gitEnv
        env.removeValue(forKey: "GH_CONFIG_DIR")
        let spec = ProcessSpec(
            executable: "/bin/zsh", args: ["-lc", command],
            cwd: cwd, env: env, stdinMode: .closed)
        do {
            let handle = try await host.launch(spec)
            self.handle = handle
            await withTaskGroup(of: Void.self) { group in
                let out = handle.stdoutLines
                let err = handle.stderrLines
                group.addTask { [weak self] in await self?.drain(out, isError: false) }
                group.addTask { [weak self] in await self?.drain(err, isError: true) }
            }
            let code = await handle.wait()
            phase = .finished(code)
        } catch {
            phase = .failed("Failed to launch: \(error.localizedDescription)")
        }
    }

    private func drain(_ stream: AsyncThrowingStream<Data, Error>, isError: Bool) async {
        do {
            for try await data in stream {
                append(String(decoding: data, as: UTF8.self), isError: isError)
            }
        } catch {}
    }

    private func append(_ text: String, isError: Bool) {
        lines.append(ConsoleLine(id: nextID, text: text, isError: isError))
        nextID += 1
        if lines.count > maxLines { lines.removeFirst(lines.count - maxLines) }
    }
}
