import AppKit
import SwiftUI

/// The welcome window (02-architecture.md § Windows; 03-board-ui.md § Welcome screen & templates).
///
/// ### Xcode's shape, which 03 says carries over unchanged
///
/// > Welcome: resizable, no title bar (background drag); recents list with board icon, name,
/// > location, counts; single click selects, double click opens; context menu Open / Reveal in
/// > Finder / Forget.
///
/// Branding and the two create/open actions on the left, recents on the right. Resizable — the
/// recents list gets whatever space the user grants it — and title-bar-less, with the background as
/// the drag surface (the gesture is attached at low priority, so a row or a button always wins).
///
/// ### The rows are derived, not assembled here
///
/// Everything a row *says* — which caption it wears, whether Open and Reveal are live, and which
/// failures had no row to land on — is `WelcomeRow.derive(recents:failures:)`, a pure function over
/// the registry's recents and `AppModel.launchFailures`. That is where 02's row-level failure rule
/// lives, and it is why the rule is testable. This file renders the answer.
///
/// ### What it never does
///
/// It never opens a board's `index.md` — not for a title, not for a count, not for an icon. Counts
/// and names come from the registry record, stamped at last close: "no directory scan at welcome
/// time (which would be slow or hang on big/unavailable boards)" (02 § Per-board app state). The
/// pathfinder loaded every board to build this list; that is the one thing about it that does not
/// carry over.
struct WelcomeView: View {

    @Environment(AppModel.self) private var appModel

    /// The selected row's record id. A row's identity is its record, so a Forget leaves this
    /// pointing at nothing, which reads as "no selection" without any cleanup of its own.
    @State private var selection: UUID?

    /// Whether the recents list holds the keyboard, so Return can mean "open the selected row".
    @FocusState private var listFocused: Bool

    // MARK: - Geometry
    //
    // Every figure this window lays out on, as a multiple of the body font — `BoardMetrics`' rule
    // applied to the welcome window (10-accessibility.md ▸ Text scaling & visual accommodations). At
    // the standard 13pt body they reproduce the numbers welcome has always drawn.

    @MainActor static var pointSize: CGFloat { BoardMetrics.bodyPointSize }

    @MainActor static var brandingColumnWidth: CGFloat { BoardMetrics.em(23, bodyPointSize: pointSize) }
    @MainActor static var brandingInset: CGFloat { BoardMetrics.em(2.5, bodyPointSize: pointSize) }
    @MainActor static var appIconSide: CGFloat { BoardMetrics.em(7.4, bodyPointSize: pointSize) }
    @MainActor static var minimumWidth: CGFloat { BoardMetrics.em(58.5, bodyPointSize: pointSize) }
    @MainActor static var minimumHeight: CGFloat { BoardMetrics.em(35.4, bodyPointSize: pointSize) }

    private var derivation: WelcomeRow.Derivation {
        WelcomeRow.derive(recents: appModel.recents, failures: appModel.launchFailures)
    }

    private var selectedRow: WelcomeRow? {
        derivation.rows.first { $0.id == selection }
    }

    var body: some View {
        HStack(spacing: 0) {
            branding
                .frame(width: Self.brandingColumnWidth)
                .frame(maxHeight: .infinity)
                .padding(Self.brandingInset)

            Divider()

            recents
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        // Font-derived, like the board window's floor (10-accessibility.md ▸ Text scaling: "no fixed
        // point sizes"): at a large system text size a 300-point branding column would clip the app
        // name it exists to show, and a 760 × 460 floor would leave the recents list too narrow for
        // the three lines each row carries. At the standard body size these are those numbers.
        .frame(minWidth: Self.minimumWidth, minHeight: Self.minimumHeight)
        // The window has no title bar, so the background is the drag handle. `.gesture` rather than
        // `.highPriorityGesture`: a click on a row or a button belongs to the row or the button.
        .gesture(WindowDragGesture())
        // Belt and braces over the explicit refreshes `AppModel` runs on every registry mutation:
        // welcome is the one surface that can appear long after the last thing that changed the list.
        .onAppear { appModel.refreshRecents() }
        // What File ▸ Reveal in Finder acts on in this window's scope (11-command-nexus.md: "welcome:
        // the selected recent's folder (disabled on unavailable rows)").
        .focusedSceneValue(\.welcomeSelection, selectedRow)
    }

    // MARK: Branding and actions

    private var branding: some View {
        VStack(alignment: .leading, spacing: 0) {
            Image(nsImage: NSApp.applicationIconImage)
                .resizable()
                .frame(width: Self.appIconSide, height: Self.appIconSide)
                .accessibilityHidden(true)

            Text("Lanework")
                // A **relative** style, not a 34pt literal — "relative text styles everywhere, no
                // fixed point sizes" (10-accessibility.md ▸ Text scaling & visual accommodations).
                // `.largeTitle` is the app name's register and it grows with the system text size;
                // a fixed size would have stayed put while every line beneath it grew past it.
                .font(.largeTitle.weight(.light))
                .padding(.top, BoardMetrics.em(0.9, bodyPointSize: Self.pointSize))

            Text(versionSummary)
                .font(.callout)
                .foregroundStyle(.secondary)

            Text("Folders and Markdown, on your terms.")
                .font(.caption)
                .foregroundStyle(.tertiary)
                .padding(.top, BoardMetrics.em(0.3, bodyPointSize: Self.pointSize))

            Spacer(minLength: BoardMetrics.em(1.85, bodyPointSize: Self.pointSize))

            VStack(spacing: BoardMetrics.em(0.6, bodyPointSize: Self.pointSize)) {
                // The menu-bar twin of this button is File ▸ New Board… (⌥⌘N) — same action, and
                // deliberately the same words, because a button and a menu item that differ read as
                // two features.
                WelcomeActionButton(title: "New Board…", systemImage: "plus.square") {
                    appModel.showTemplateChooser()
                }
                WelcomeActionButton(title: "Open Board…", systemImage: "folder") {
                    appModel.presentOpenPanel()
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private var versionSummary: String {
        let info = Bundle.main.infoDictionary
        let short = info?["CFBundleShortVersionString"] as? String ?? "—"
        let build = info?["CFBundleVersion"] as? String ?? "—"
        return "Version \(short) (\(build))"
    }

    // MARK: Recents

    @ViewBuilder
    private var recents: some View {
        let derivation = self.derivation

        VStack(spacing: 0) {
            if derivation.rows.isEmpty {
                emptyHint
            } else {
                list(derivation.rows)
            }

            if !derivation.unmatched.isEmpty {
                Divider()
                unmatchedFailures(derivation.unmatched)
            }
        }
        .background(Color(nsColor: .controlBackgroundColor))
    }

    private var emptyHint: some View {
        VStack(spacing: 6) {
            Image(systemName: "clock")
                .font(.title)
                .foregroundStyle(.tertiary)
            Text("No Recent Boards")
                .font(.headline)
                .foregroundStyle(.secondary)
            Text("Boards you create or open appear here.")
                .font(.caption)
                .foregroundStyle(.tertiary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func list(_ rows: [WelcomeRow]) -> some View {
        List(rows, selection: $selection) { row in
            RecentBoardRow(row: row)
                // Single click selects (the `List` does that); the second click of a double click
                // opens. `simultaneousGesture` rather than `onTapGesture` so the list's own
                // selection handling still sees the first click.
                .simultaneousGesture(TapGesture(count: 2).onEnded { open(row) })
                .contextMenu {
                    Button("Open") { open(row) }
                        .disabled(!row.canOpen)
                    Button("Reveal in Finder") { reveal(row) }
                        .disabled(!row.canReveal)
                    Divider()
                    // Always enabled, on every row: an orphan the user can never open again is
                    // exactly the row that most needs erasing (02 § Graceful orphaning).
                    Button("Forget") { appModel.forget(boardID: row.id) }
                }
        }
        .listStyle(.inset)
        .scrollContentBackground(.hidden)
        .focused($listFocused)
        // Return on a selected row opens it — the list convention, and the reason the list takes
        // focus on a click rather than only on Tab.
        .onKeyPress(.return) {
            guard let selectedRow, selectedRow.canOpen else { return .ignored }
            open(selectedRow)
            return .handled
        }
        .onTapGesture { listFocused = true }
    }

    /// The failures no recents row could carry — a first open of a folder that turned out not to be
    /// a board fails before anything is registered, so there is no row for it to land on. A list of
    /// their own, because the alternative is the silent drop 02 rules out.
    private func unmatchedFailures(_ failures: [LaunchFailure]) -> some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Couldn't Open")
                .font(.subheadline.weight(.semibold))

            ForEach(failures) { failure in
                VStack(alignment: .leading, spacing: 1) {
                    Text(failure.displayName)
                        .font(.callout)
                    Text(failure.message)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                    Text(failure.path)
                        .font(.caption)
                        .foregroundStyle(.tertiary)
                        .lineLimit(1)
                        .truncationMode(.middle)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }

            // Clears exactly the failures listed here, never the ones standing on rows above: those
            // are still describing a board the user can see, and one button quietly erasing both
            // lists would be the drop 02 forbids wearing a different hat.
            Button("Clear") {
                appModel.clearLaunchFailures(ids: Set(failures.map(\.id)))
            }
            .controlSize(.small)
        }
        .padding(BoardMetrics.em(1.25, bodyPointSize: Self.pointSize))
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    // MARK: Actions

    /// Opens a row's board. Welcome closes itself on the way in — that is the board window host's
    /// job ("Opening a board from welcome closes welcome"), not this view's, because the close has to
    /// wait for the load to actually succeed.
    private func open(_ row: WelcomeRow) {
        guard let url = row.url else { return }
        appModel.openBoard(at: url)
    }

    private func reveal(_ row: WelcomeRow) {
        guard let url = row.url else { return }
        NSWorkspace.shared.activateFileViewerSelecting([url])
    }
}

// MARK: - Pieces

/// A full-width, leading-aligned action button — Xcode's welcome column.
private struct WelcomeActionButton: View {

    let title: String
    let systemImage: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Label(title, systemImage: systemImage)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(.vertical, BoardMetrics.em(0.15, bodyPointSize: WelcomeView.pointSize))
        }
        .buttonStyle(.bordered)
        .controlSize(.large)
    }
}

/// One recents row: icon, name, location, and the one caption line carrying whichever of the three
/// things the row has to say (`WelcomeRow.Caption`).
private struct RecentBoardRow: View {

    let row: WelcomeRow

    var body: some View {
        HStack(spacing: 12) {
            // The board's own icon and tint — registry-cached with live write-through (02 §
            // Per-board app state: "the row's title and icon are registry-cached too — with live
            // write-through"). A record with no override, or one naming a symbol this system
            // cannot draw, falls back to the board-default glyph in secondary — the same
            // lenient-fallback shape every other icon site in the app uses (`ItemSymbol`,
            // `Palette`), applied here to the registry's cached string instead of a live snapshot.
            Image(systemName: iconName)
                // Relative, like every other size in this window (10-accessibility.md's
                // full-relative-scaling rule): `.title` is the register a 22pt glyph occupied at the
                // standard text size, and the well around it is derived from the body font so the
                // glyph never outgrows it.
                .font(.title)
                .foregroundStyle(iconTint)
                .frame(width: iconWell, height: iconWell)
                .accessibilityHidden(true)

            VStack(alignment: .leading, spacing: 1) {
                Text(row.displayName)
                    .font(.headline)
                    .lineLimit(1)

                Text(row.location)
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
                    .truncationMode(.middle)

                caption
            }

            Spacer(minLength: 0)
        }
        .padding(.vertical, BoardMetrics.em(0.3, bodyPointSize: WelcomeView.pointSize))
        // Dimmed when the board cannot be reached — the row stays, with Forget, rather than
        // disappearing (02 § Graceful orphaning).
        .opacity(row.isAvailable ? 1 : 0.55)
        .accessibilityElement(children: .combine)
    }

    /// The square the row's glyph sits in — font-derived so the icon column stays proportionate to
    /// the three lines of text beside it at every system text size.
    private var iconWell: CGFloat {
        BoardMetrics.em(2.6, bodyPointSize: BoardMetrics.bodyPointSize)
    }

    /// `row.icon`'s symbol if it names one this system can draw, the board default otherwise —
    /// `ItemSymbol.name(_:fallback:)`'s rule, restated for a plain cached string rather than a
    /// `FieldValue`: a record carries no `FieldValue`, so `missing`/`malformed`/`unrecognized`
    /// have already folded into one `nil` by the time it reaches here.
    private var iconName: String {
        guard let icon = row.icon, ItemSymbol.exists(icon) else { return ItemSymbol.board }
        return icon
    }

    /// `row.iconColor`'s tint, or the standard secondary one — `LaneView`'s card-face `iconTint`,
    /// same fallback, same reason: an uncoloured icon is chrome, and chrome is secondary.
    private var iconTint: AnyShapeStyle {
        if let iconColor = row.iconColor, let color = Palette.color(named: iconColor) {
            AnyShapeStyle(color)
        } else {
            AnyShapeStyle(.secondary)
        }
    }

    @ViewBuilder
    private var caption: some View {
        switch row.caption {
        case .counts:
            Text(row.countsSummary)
                .font(.caption)
                .foregroundStyle(.tertiary)
        case .unavailable:
            Label("Unavailable — moved, deleted, or on a volume that isn't mounted",
                  systemImage: "questionmark.folder")
                .font(.caption)
                .foregroundStyle(.secondary)
                .lineLimit(1)
        case let .failed(message):
            // The warning tint, and the whole of fail-fast's specifics — this row *is* the failure
            // surface (02 § Launch and window lifecycle).
            Label(message, systemImage: "exclamationmark.triangle.fill")
                .font(.caption)
                .foregroundStyle(.orange)
                .lineLimit(2)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

// MARK: - Settings

/// The app's preferences (⌘, — 11-command-nexus.md).
///
/// One section, and one section is all there is: "Restore open boards at launch", whose preference
/// gates only whether the registry's open-now flags are *consulted* at launch — the flags themselves
/// are maintained either way, which is what keeps crash recovery working for a user who has
/// restoration turned off and then turns it back on.
///
/// **There is no purchase section, and nothing to build one out of** (12-editions.md ▸ PIVOT
/// 2026-08-08 — one version, everything free): the subscription machinery this pane once hosted is
/// excised, not dormant. A paid tier returns as a fresh design pass, with the iPhone companion and
/// sync, and will bring its own surface.
struct SettingsView: View {

    @AppStorage(AppPreferences.restoreOpenBoardsAtLaunchKey)
    private var restoreOpenBoardsAtLaunch = true

    var body: some View {
        Form {
            Section {
                Toggle("Restore open boards at launch", isOn: $restoreOpenBoardsAtLaunch)
            } footer: {
                Text("On, the boards open at your last quit reopen automatically. Off, every launch starts at Welcome.")
            }
        }
        .formStyle(.grouped)
        // Font-derived: this pane is `.fixedSize()`, so a 420-point literal would clip a footer
        // sentence at a large system text size with no way to resize out of it
        // (10-accessibility.md ▸ Text scaling).
        .frame(width: BoardMetrics.em(32.3, bodyPointSize: BoardMetrics.bodyPointSize))
        .fixedSize()
    }
}
