import SwiftUI
import NucleicProtocol

/// Start a new chat, revealed as a floating bar when the "+" is tapped (see `NewChatAffordance`).
/// Styled as the same Liquid Glass composer bar the open session carries, for consistency: picks a
/// project (or is scoped to one), types a prompt, optionally toggles Auto.
struct StartChatComposer: View {
    @EnvironmentObject var store: RemoteStore
    /// The prompt text, owned by `NewChatAffordance` so it can coordinate auto-focus and the
    /// close-on-empty-dismiss behaviour with the composer.
    @Binding var draft: String
    /// The prompt field's focus, likewise owned by the affordance (auto-focused on open, resigned
    /// to dismiss the keyboard).
    var focus: FocusState<Bool>.Binding
    /// Called after a chat is kicked off, so the affordance can dismiss the composer.
    var onStart: (() -> Void)? = nil
    /// When set, the composer is scoped to this project and the picker is hidden — used by a
    /// project page's "+", so the chat always lands in that project.
    var lockedProject: WireProject? = nil
    @State private var projectID: ProjectID?
    @State private var autoOverride: Bool?
    @State private var model: String?
    @State private var effort = MobileEfforts.fallback
    // Advanced options (the Mac new-session sheet's base branch + worktree fields).
    @State private var showOptions = false
    @State private var baseBranch = ""
    @State private var useWorktree = true
    // Files/images attached to the opening message (see `ComposerAttachments`) — shipped as bytes
    // and materialized into the new session's working tree host-side.
    @State private var attachments: [StagedAttachment] = []
    @State private var attachmentsOverflowed = false

    private var projects: [WireProject] { store.dashboard.projects }
    /// The picker defaults to "No project" (nil) rather than auto-selecting the first project, so a
    /// typed prompt never quietly starts an agent in a repo the user didn't choose — they pick one
    /// explicitly before the send button lights. A project page's composer stays scoped via
    /// `lockedProject`.
    private var selected: WireProject? {
        lockedProject ?? projects.first { $0.id == projectID }
    }
    private var controlled: Bool { selected?.isNucleicControlled ?? false }
    /// Auto-approve for the chat about to start: the user's choice off-control, but forced on for a
    /// Nucleic Control project. The host locks Auto on for those chats regardless of what the phone
    /// sends (they run autonomously — nvrsion needs it: see `AppStore.createSession`), so the
    /// composer shows it locked on to match the Mac rather than offering a toggle that wouldn't take.
    private var effectiveAuto: Bool { controlled || (autoOverride ?? false) }

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                if lockedProject == nil {
                    Menu {
                        Button("No project") { projectID = nil }
                        if !projects.isEmpty { Divider() }
                        ForEach(projects) { project in
                            Button(project.name) { projectID = project.id }
                        }
                    } label: {
                        // The model also selects the backend, so the picker offers every project.
                        // Defaults to "No project" until one is chosen.
                        Label(selected?.name ?? "No project", systemImage: "folder")
                            .font(.subheadline)
                    }
                } else {
                    // Scoped to this project's page — show it as context, not a picker.
                    Label(selected?.name ?? "", systemImage: "folder")
                        .font(.subheadline).foregroundStyle(.secondary)
                }
                Spacer()
                Toggle(isOn: Binding(get: { effectiveAuto }, set: { autoOverride = $0 })) {
                    Label("Auto", systemImage: effectiveAuto ? "bolt.fill" : "bolt.slash")
                }
                .toggleStyle(.button)
                .tint(Palette.accent)
                .font(.caption)
                // Locked on for a Nucleic Control project — those chats always run autonomously.
                .disabled(controlled)
                .help(controlled
                    ? "Auto-approve is always on for Nucleic Control chats — they run autonomously."
                    : "Auto-approve safe actions; destructive ones still ask.")
            }
            HStack {
                ModelMenu(model: $model, catalog: store.modelCatalog, backend: nil)
                Spacer()
                EffortMenu(effort: $effort, catalog: store.modelCatalog, modelSKU: model, controlled: controlled)
                Button {
                    withAnimation(.easeInOut(duration: 0.18)) { showOptions.toggle() }
                } label: {
                    Image(systemName: showOptions ? "chevron.up.circle" : "ellipsis.circle")
                        .font(.caption)
                        .foregroundStyle(Palette.accent)
                }
                .buttonStyle(.plain)
            }
            if showOptions {
                VStack(alignment: .leading, spacing: 8) {
                    HStack(spacing: 8) {
                        Image(systemName: "arrow.triangle.branch").foregroundStyle(.secondary)
                        TextField(selected?.defaultBranch ?? "Base branch", text: $baseBranch)
                            .textInputAutocapitalization(.never)
                            .autocorrectionDisabled()
                            .textFieldStyle(.roundedBorder)
                            .font(.caption)
                    }
                    Toggle(isOn: $useWorktree) {
                        Label("Run in a worktree", systemImage: "square.stack.3d.up")
                            .font(.caption)
                    }
                    .tint(Palette.accent)
                }
                .transition(.move(edge: .top).combined(with: .opacity))
            }
            // Staged attachments ride above the field, matching the in-session composer.
            if !attachments.isEmpty {
                StagedAttachmentBar(attachments: $attachments)
            }
            if attachmentsOverflowed {
                Text("Some files were too large to attach.")
                    .font(.caption2).foregroundStyle(.secondary)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
            HStack(alignment: .bottom, spacing: 8) {
                // Attach photos/files to the opening message.
                AttachMenuButton(attachments: $attachments, overflowed: $attachmentsOverflowed)
                // Plain, borderless field — matching the open session's chat bar rather than a
                // boxed form control.
                TextField("Describe a task…", text: $draft, axis: .vertical)
                    .textFieldStyle(.plain)
                    .lineLimit(1...5)
                    .padding(.vertical, 3)
                    .orchestraGlow(active: MobileEfforts.isOrchestra(effort) && controlled)
                    .focused(focus)
                Button {
                    if let project = selected {
                        let branch = baseBranch.trimmingCharacters(in: .whitespaces)
                        store.startChat(
                            in: project.id, message: draft, model: model, effort: effort,
                            baseBranch: branch.isEmpty ? nil : branch,
                            useWorktree: useWorktree, auto: effectiveAuto,
                            attachments: attachments.wireAttachments)
                        draft = ""
                        attachments = []
                        attachmentsOverflowed = false
                        onStart?()
                    }
                } label: {
                    Image(systemName: "arrow.up.circle.fill").font(.title)
                }
                // An attachment-only opening message is allowed (a project must still be chosen).
                .disabled(selected == nil || (draft.trimmingCharacters(in: .whitespaces).isEmpty && attachments.isEmpty) || !store.canControl)
                .keyboardShortcut(.return, modifiers: .command)
            }
            if !store.canControl {
                Text("This device is view-only.").font(.caption2).foregroundStyle(.secondary)
            }
        }
        // The same Liquid Glass bar the open session carries, so the new-chat entry box is visually
        // identical to the in-chat composer.
        .padding(.horizontal, 14)
        .padding(.vertical, 12)
        .glassSurface(cornerRadius: 26)
        // Prefill the base branch with the project's default so it actually appears (mirroring the
        // Mac composer's `effectiveBranch`), rather than sitting as an empty placeholder.
        .onAppear(perform: syncToProject)
        .onChange(of: projectID) { _, _ in syncToProject() }
        // Switching to a model with a lower effort cap can't leave an unsupported level selected.
        .onChange(of: model) { _, newModel in
            let levels = store.modelCatalog.offeredEfforts(forModel: newModel)
            let sentinel = store.modelCatalog.orchestraSentinelOrFallback
            if effort.caseInsensitiveCompare(sentinel) != .orderedSame, !levels.contains(effort) {
                effort = levels.last ?? MobileEfforts.fallback
            }
        }
    }

    /// Sync the composer to the freshly-selected project: show its default branch as the base, and
    /// resolve a carried-over Orchestra selection down to a real level when the new project isn't
    /// under Nucleic Control (Orchestra can't run there — mirrors the Mac's `effectiveEffort`).
    private func syncToProject() {
        baseBranch = selected?.defaultBranch ?? ""
        if MobileEfforts.isOrchestra(effort), !controlled {
            effort = store.modelCatalog.offeredEfforts(forModel: model).last ?? MobileEfforts.fallback
        }
    }
}

/// The floating "+" start-chat affordance: a Liquid Glass circle in the lower-right that reveals
/// the glass `StartChatComposer` as a floating bar over the page — the same entry box the open
/// session carries, not a full modal pane. Shared by Home, Sessions, and a project page (which
/// scopes it to `project`), so all three offer the same control.
struct NewChatAffordance: ViewModifier {
    @EnvironmentObject var store: RemoteStore
    /// Scopes the composer to one project (project page); nil offers the picker (Home, Sessions).
    var project: WireProject? = nil
    /// Owned by the caller so it can also open the composer programmatically (iPad ⌘N on Home,
    /// the compact shell's shared tab-bar "+").
    @Binding var isPresented: Bool
    /// Whether to draw the floating "+" disc. The compact shell hosts its own "+" beside the tab
    /// bar and only wants the composer presentation, so it passes `false`; per-page callers (iPad)
    /// keep their own disc.
    var showsButton: Bool = true
    /// Prompt text and its focus live here so the scrim tap and auto-focus can coordinate with the
    /// composer (whether it's empty, whether the keyboard is up).
    @State private var draft = ""
    @FocusState private var draftFocused: Bool

    /// The button only makes sense when this device can control a Mac and there's a project to
    /// start a chat in; otherwise the composer it opens would be inert.
    private var canStart: Bool {
        store.canControl && (project != nil || !store.dashboard.projects.isEmpty)
    }

    private var isEmpty: Bool { draft.trimmingCharacters(in: .whitespaces).isEmpty }

    func body(content: Content) -> some View {
        content
            // The "+" disc — hidden while the composer is up so it doesn't sit over the bar.
            .overlay(alignment: .bottomTrailing) {
                if showsButton && canStart && !isPresented {
                    Button { isPresented = true } label: {
                        Image(systemName: "plus")
                            .font(.system(size: 22, weight: .semibold))
                            .foregroundStyle(Palette.accent)
                            .frame(width: 56, height: 56)
                            // The whole disc is the hit target — without this, taps on the
                            // translucent glass outside the glyph fall through to the list row
                            // behind the button.
                            .contentShape(Circle())
                            .glassCircle()
                    }
                    .buttonStyle(.plain)
                    .padding(.trailing, 20)
                    .padding(.bottom, 6)
                    .transition(.scale.combined(with: .opacity))
                    .accessibilityLabel("New chat")
                }
            }
            // The composer itself, floating up from the bottom over a light scrim — no modal pane,
            // no nav chrome, just the entry box.
            .overlay {
                if isPresented {
                    ZStack(alignment: .bottom) {
                        Rectangle()
                            .fill(.black.opacity(0.2))
                            .ignoresSafeArea()
                            .transition(.opacity)
                            // Tapping away drops the keyboard; with nothing typed there's nothing to
                            // keep the bar around for, so close it too. A draft in progress stays
                            // open (keyboard down) so a stray tap doesn't discard it — a second tap
                            // (keyboard already down) then closes it.
                            .onTapGesture {
                                if draftFocused {
                                    draftFocused = false
                                    if isEmpty { isPresented = false }
                                } else {
                                    isPresented = false
                                }
                            }
                        StartChatComposer(draft: $draft, focus: $draftFocused,
                                          onStart: { isPresented = false }, lockedProject: project)
                            // Taps on the bar's own empty spots (rounded corners, gaps between
                            // controls) must not fall through to the scrim and close it — only taps
                            // outside the bar dismiss. A clear tap-catcher sized to the bar absorbs
                            // them; the controls, in front, still handle their own taps.
                            .background(Color.clear.contentShape(Rectangle()).onTapGesture {})
                            .padding(.horizontal, 12)
                            .padding(.bottom, 8)
                            .transition(.move(edge: .bottom).combined(with: .opacity))
                            // A deliberate swipe down dismisses the bar (drops the keyboard first),
                            // the gestural twin of tapping the scrim. minimumDistance keeps taps on
                            // the field / menus / send button working; the vertical-dominant check
                            // ignores horizontal drags (e.g. moving the text cursor).
                            .gesture(
                                DragGesture(minimumDistance: 20)
                                    .onEnded { value in
                                        if value.translation.height > 60,
                                           value.translation.height > abs(value.translation.width) {
                                            draftFocused = false
                                            isPresented = false
                                        }
                                    }
                            )
                    }
                }
            }
            .animation(.spring(response: 0.35, dampingFraction: 0.86), value: isPresented)
            .onChange(of: isPresented) { _, open in
                if open {
                    draft = ""
                    // Focus in the same turn the bar is presented so the keyboard rises immediately,
                    // riding up together with the bar (both animate from the bottom edge at t=0)
                    // instead of waiting for the spring to settle first — no perceptible delay
                    // between the composer appearing and the keyboard opening.
                    draftFocused = true
                } else {
                    draftFocused = false
                    draft = ""
                }
            }
    }
}

extension View {
    /// Add the floating "+" start-chat affordance (glass circle → glass composer sheet). See
    /// `NewChatAffordance`.
    /// `enabled: false` drops the affordance entirely — the compact shell hosts a single shared "+"
    /// beside its tab bar, so the per-page discs (Home, a project page) apply this only at regular
    /// width, where there's no tab bar to carry it.
    @ViewBuilder
    func newChatAffordance(project: WireProject? = nil, isPresented: Binding<Bool>,
                           showsButton: Bool = true, enabled: Bool = true) -> some View {
        if enabled {
            modifier(NewChatAffordance(project: project, isPresented: isPresented, showsButton: showsButton))
        } else {
            self
        }
    }
}

/// A compact peek at open ideas with inline capture; the full inbox is the To-Dos tab.
struct QuickTodos: View {
    @EnvironmentObject var store: RemoteStore
    @State private var draft = ""

    private var open: [WireTodo] { store.dashboard.todos.filter { $0.status == .open } }

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text("To-dos").font(.headline)
                Spacer()
                if !open.isEmpty { Text("\(open.count)").foregroundStyle(.secondary) }
            }
            HStack {
                TextField("Capture an idea…", text: $draft)
                    .textFieldStyle(.roundedBorder)
                    .onSubmit(capture)
                Button(action: capture) { Image(systemName: "plus.circle.fill").font(.title3) }
                    .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.canControl)
            }
            ForEach(open.prefix(3)) { todo in
                HStack(spacing: 10) {
                    Button { store.completeTodo(todo.id) } label: {
                        Image(systemName: "circle").foregroundStyle(.secondary)
                    }.buttonStyle(.plain)
                    Text(todo.summary?.isEmpty == false ? todo.summary! : todo.text)
                        .font(.subheadline).lineLimit(1)
                    if let triage = todo.triage { TriageBadge(rawLevel: triage) }
                    Spacer()
                    if let name = todo.projectName {
                        Text(name).font(.caption2).foregroundStyle(.secondary)
                    }
                }
            }
        }
        .card()
    }

    private func capture() {
        store.captureTodo(draft, projectID: nil)
        draft = ""
    }
}
