import SwiftUI
import SafariServices
import NucleicProtocol

// Remote agent sign-in, phone UI (docs/REMOTE_AGENT_LOGIN.md §6): the Agent Accounts section in
// Settings (per-host provider status + Sign in), the sheet that drives one attempt (in-app
// Safari + loopback auto-capture, or Claude's paste-a-code fallback), and the in-chat banner
// that offers a sign-in when a turn dies on an auth failure.

/// The Settings ▸ Agent Accounts section: every live host's per-provider install/auth state
/// (from its `agentAuthStatus` push) with a Sign in button where that host can broker the flow.
struct AgentAccountsSection: View {
    @EnvironmentObject var store: RemoteStore
    /// The row whose "Use API key…" sheet is open — (host, provider, display name).
    @State private var apiKeyTarget: APIKeyTarget?
    /// The mesh-wide deletion awaiting the user's confirm (it tombstones the credential on
    /// EVERY device, so it always confirms first). Nil hides the dialog.
    @State private var deleteTarget: DeleteTarget?

    struct APIKeyTarget: Identifiable {
        let hostID: String
        let hostName: String
        let provider: AgentLoginProvider
        let providerName: String
        var id: String { hostID + "·" + provider.rawValue }
    }

    struct DeleteTarget: Identifiable {
        let kind: CredentialKind
        let label: String
        var id: String { kind.rawValue }
    }

    var body: some View {
        let hosts = store.agentAccountHosts
        if !hosts.isEmpty {
            Section {
                ForEach(hosts, id: \.hostID) { host in
                    if hosts.count > 1 {
                        Text(host.name)
                            .font(.footnote.weight(.semibold))
                            .foregroundStyle(.secondary)
                            .textCase(.uppercase)
                    }
                    ForEach(host.statuses, id: \.provider.rawValue) { status in
                        providerRow(status, hostID: host.hostID, hostName: host.name)
                    }
                }
            } header: {
                Text("Agent accounts")
            } footer: {
                Text(footerText)
            }
            .sheet(item: $apiKeyTarget) { target in
                APIKeyEntrySheet(target: target)
            }
            .confirmationDialog(
                "Delete the \(deleteTarget?.label ?? "credential") from every device in your mesh?",
                isPresented: Binding(
                    get: { deleteTarget != nil },
                    set: { if !$0 { deleteTarget = nil } }),
                titleVisibility: .visible
            ) {
                Button("Delete Everywhere", role: .destructive) {
                    guard let target = deleteTarget else { return }
                    deleteTarget = nil
                    store.revokeCredential(kind: target.kind)
                }
                Button("Cancel", role: .cancel) { deleteTarget = nil }
            } message: {
                Text("Your Macs, cloud runners, and this iPhone all drop it. A tombstone keeps "
                     + "any offline device from bringing it back; signing in again re-enables "
                     + "the provider everywhere.")
            }
        }
    }

    /// The section footer: the standard sign-in explainer, plus — once this phone actually
    /// holds mirrored logins — the holder note (an encrypted copy lives here, so a fresh
    /// runner can be credentialed with every Mac asleep).
    private var footerText: String {
        var text = "Sign-ins run on the host — your Mac or a cloud runner — and sync to every "
            + "device in your mesh. This phone only shows the consent page and relays "
            + "the sign-in code over the encrypted channel."
        let held = store.phoneVaultKinds.compactMap { kind -> String? in
            switch kind {
            case .claudeOAuth: "Claude"
            case .codexAuth: "Codex"
            default: nil
            }
        }
        if !held.isEmpty {
            text += " This iPhone also keeps an encrypted copy of the "
                + held.joined(separator: " and ")
                + " sign-in, so it can credential a fresh runner on its own."
        }
        return text
    }

    @ViewBuilder
    private func providerRow(
        _ status: WireProviderAuthStatus, hostID: String, hostName: String
    ) -> some View {
        let name = status.name.isEmpty ? status.provider.rawValue.capitalized : status.name
        HStack(spacing: 10) {
            Image(systemName: status.authenticated ? "checkmark.seal.fill" : "person.crop.circle.badge.questionmark")
                .foregroundStyle(status.authenticated ? Color.green : Color.secondary)
            VStack(alignment: .leading, spacing: 2) {
                Text(name)
                Text(detail(for: status))
                    .font(.footnote)
                    .foregroundStyle(.secondary)
            }
            Spacer()
            if status.canBrokerLogin {
                Button(status.authenticated ? "Sign in again" : "Sign in") {
                    store.beginAgentLogin(provider: status.provider, onHost: hostID)
                }
                .buttonStyle(.borderless)
                .font(.callout)
            }
            // The ToS-defensive fallback (REMOTE_AGENT_LOGIN §8): set a Console/API key
            // instead of a subscription login. Only for the two key-backed providers, and only
            // when the host can take a sealed key from this phone.
            if apiKeyProviders.contains(status.provider), store.canSubmitAPIKey(toHost: hostID) {
                Button {
                    apiKeyTarget = APIKeyTarget(
                        hostID: hostID, hostName: hostName,
                        provider: status.provider, providerName: name)
                } label: {
                    Image(systemName: "key")
                }
                .buttonStyle(.borderless)
                .accessibilityLabel("Use an API key for \(name)")
            }
        }
        .contextMenu {
            // Mesh-wide deletion (key deletion from any device): offered when a host that
            // accepts the tombstone verb is reachable — the deletion then propagates from it
            // to every other member (and this phone clears its own vault copy regardless).
            if status.authenticated, store.canRevokeCredentials(onHost: hostID) {
                if status.method == "apiKey" {
                    Button(role: .destructive) {
                        deleteTarget = DeleteTarget(
                            kind: status.provider == .codex ? .openAIAPIKey : .anthropicAPIKey,
                            label: "\(name) API key")
                    } label: {
                        Label("Delete API Key on All Devices…", systemImage: "trash")
                    }
                } else {
                    Button(role: .destructive) {
                        deleteTarget = DeleteTarget(
                            kind: status.provider == .codex ? .codexAuth : .claudeOAuth,
                            label: "\(name) sign-in")
                    } label: {
                        Label("Sign Out on All Devices…", systemImage: "trash")
                    }
                }
            }
        }
    }

    private var apiKeyProviders: [AgentLoginProvider] { [.claude, .codex] }

    private func detail(for status: WireProviderAuthStatus) -> String {
        switch (status.installed, status.authenticated) {
        case (true, true):
            if let label = status.accountLabel, !label.isEmpty { return "Signed in · \(label)" }
            if status.method == "apiKey" { return "Signed in · API key" }
            return "Signed in"
        case (true, false): return "Installed, not signed in"
        case (false, _): return "Not installed on this host"
        }
    }
}

/// SecureField entry for a provider API key, sealed straight to the chosen host
/// (REMOTE_AGENT_LOGIN §8). The phone never stores the key; confirmation is implicit — the
/// provider row flips to "Signed in · API key" when the host's refreshed status push lands.
private struct APIKeyEntrySheet: View {
    @EnvironmentObject var store: RemoteStore
    @Environment(\.dismiss) private var dismiss
    let target: AgentAccountsSection.APIKeyTarget

    @State private var key = ""
    @State private var sent = false
    @State private var failed = false

    private var keyName: String {
        target.provider == .claude ? "Anthropic API key" : "OpenAI API key"
    }

    var body: some View {
        NavigationStack {
            Form {
                if sent {
                    Section {
                        Label("Key sent to \(target.hostName)", systemImage: "checkmark.seal.fill")
                            .foregroundStyle(.green)
                        Text("The \(target.providerName) row will show “API key” once it lands.")
                            .font(.footnote).foregroundStyle(.secondary)
                    }
                } else {
                    Section {
                        SecureField(keyName, text: $key)
                            .autocorrectionDisabled()
                            .textInputAutocapitalization(.never)
                        if failed {
                            Text("Couldn't send the key — the host may have disconnected. Try again.")
                                .font(.footnote).foregroundStyle(.red)
                        }
                    } footer: {
                        Text("Sealed to \(target.hostName) over the encrypted channel and stored "
                             + "there — never on this phone. Replaces any key already set.")
                    }
                }
            }
            .navigationTitle("\(target.providerName) API key")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button(sent ? "Done" : "Cancel") { dismiss() }
                }
                if !sent {
                    ToolbarItem(placement: .confirmationAction) {
                        Button("Save") {
                            let ok = store.submitAPIKey(
                                key, provider: target.provider, toHost: target.hostID)
                            sent = ok
                            failed = !ok
                        }
                        .disabled(key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                    }
                }
            }
        }
        .presentationDetents([.medium])
    }
}

/// Drives one sign-in attempt end to end, rendering whatever the flow state asks for: a spinner
/// while the host builds the challenge, the vendor's consent page (with the loopback armed for
/// auto-capture, or a paste bar for Claude's console fallback), and the final outcome.
struct AgentLoginSheet: View {
    @EnvironmentObject var store: RemoteStore
    @State private var pastedCode = ""

    private var providerLabel: String {
        switch store.agentLoginProvider {
        case .some(.claude): "Claude"
        case .some(.codex): "Codex"
        case .some(let other): other.rawValue.capitalized
        case .none: "Agent"
        }
    }

    var body: some View {
        NavigationStack {
            content
                .navigationTitle("Sign in to \(providerLabel)")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button(isDone ? "Done" : "Cancel") { store.cancelAgentLogin() }
                    }
                }
        }
        .interactiveDismissDisabled(!isDone)
    }

    private var isDone: Bool {
        if case .done = store.agentLogin { return true }
        return false
    }

    @ViewBuilder
    private var content: some View {
        switch store.agentLogin {
        case .idle, .starting:
            ProgressView("Contacting host…")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        case .browser(let url):
            SafariView(url: url)
                .ignoresSafeArea(edges: .bottom)
        case .pasteCode(let url):
            SafariView(url: url)
                .ignoresSafeArea(edges: .bottom)
                .safeAreaInset(edge: .bottom) { pasteBar }
        case .finishing:
            ProgressView("Completing sign-in…")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        case .done(let success, let message):
            VStack(spacing: 12) {
                Image(systemName: success ? "checkmark.seal.fill" : "exclamationmark.triangle.fill")
                    .font(.system(size: 44))
                    .foregroundStyle(success ? Color.green : Color.orange)
                Text(success
                     ? "\(providerLabel) is signed in. Every device in your mesh can use it."
                     : (message ?? "Sign-in did not complete."))
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 24)
                Button("Done") { store.cancelAgentLogin() }
                    .buttonStyle(.borderedProminent)
                    .padding(.top, 8)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }

    /// Claude's console fallback renders a `code#state` blob on the consent page — the user
    /// copies it there and pastes it here; the host does the exchange.
    private var pasteBar: some View {
        HStack(spacing: 8) {
            TextField("Paste the code shown by the sign-in page", text: $pastedCode)
                .textFieldStyle(.roundedBorder)
                .autocorrectionDisabled()
                .textInputAutocapitalization(.never)
            Button("Submit") {
                store.submitPastedLoginCode(pastedCode)
            }
            .buttonStyle(.borderedProminent)
            .disabled(pastedCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
        }
        .padding(10)
        .background(.bar)
    }
}

/// In-app Safari for the vendor's consent page. `SFSafariViewController` (not
/// `ASWebAuthenticationSession`) on purpose: its callback API can't intercept a plain
/// `http://localhost` redirect, and keeping the app foreground keeps the loopback listener and
/// the host socket alive for the whole round-trip — Safari-on-device resolves `localhost` to
/// this phone, which is the entire capture trick.
struct SafariView: UIViewControllerRepresentable {
    let url: URL

    func makeUIViewController(context: Context) -> SFSafariViewController {
        let controller = SFSafariViewController(url: url)
        controller.dismissButtonStyle = .cancel
        return controller
    }

    func updateUIViewController(_ controller: SFSafariViewController, context: Context) {}
}

/// The auth-failure matcher the in-chat banner keys on — mirrors the Mac's
/// `TranscriptRow.isAuthError` so both surfaces light up on the same failures.
enum AgentAuthErrors {
    static func isAuthError(_ text: String) -> Bool {
        let lowered = text.lowercased()
        return lowered.contains("401")
            || lowered.contains("authentication_error")
            || lowered.contains("authentication error")
            || lowered.contains("not logged in")
            || lowered.contains("oauth token has expired")
            || lowered.contains("please run /login")
            || lowered.contains("invalid api key")
            || lowered.contains("credential")
            && lowered.contains("expired")
    }

    /// The seq of the auth failure the transcript's tail ended on, or nil if it didn't — the
    /// banner's trigger. Only the events after the last completed run matter: a re-auth mid-history
    /// shouldn't nag. Returning the seq (not just a Bool) lets the banner suppress a failure a
    /// completed sign-in has already resolved while still re-arming for a *newer* one.
    static func authFailureSeq(_ events: [AgentEvent]) -> UInt64? {
        for event in events.suffix(30).reversed() {
            switch event.kind {
            case .runFinished(let finished):
                guard finished.outcome == .errored else { return nil }
                return (finished.finalText.map(isAuthError) ?? false) ? event.seq : nil
            case .error(let error):
                if isAuthError(error.message) { return event.seq }
            default:
                continue
            }
        }
        return nil
    }
}

/// The in-chat re-auth affordance: shown above the composer when the open session's last run
/// died on an auth failure and a connected host can broker the matching provider's sign-in.
struct AgentAuthErrorBanner: View {
    @EnvironmentObject var store: RemoteStore
    let sessionID: SessionID
    let backend: BackendID

    var body: some View {
        // Show only while the transcript's tail still ends on an auth failure that a completed
        // sign-in hasn't already resolved. A successful sign-in anchors `authResolvedSeq` at the
        // tail's newest seq, so the banner clears the instant login finishes — no waiting for a
        // fresh run to rewrite the tail — while a genuinely newer failure (a higher seq) re-arms it.
        if AgentLoginProvider.forBackend(backend) != nil,
           let failSeq = AgentAuthErrors.authFailureSeq(store.openEvents),
           failSeq > store.authResolvedSeq(forSession: sessionID)
        {
            HStack(spacing: 10) {
                Image(systemName: "key.fill")
                    .foregroundStyle(.orange)
                Text("The agent isn't signed in.")
                    .font(.callout)
                Spacer()
                Button("Sign in") {
                    store.beginAgentLogin(forSession: sessionID)
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.small)
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 8)
            .background(.orange.opacity(0.12), in: RoundedRectangle(cornerRadius: 10))
            .padding(.horizontal, 12)
        }
    }
}
