import SwiftUI
import AppKit
import NucleicCore

/// App-wide surface colors. Each adapts to the active appearance (a deep,
/// high-contrast dark — darker than the system dark material — and a clean light),
/// so the same `AppTheme.x` works in light, dark, or system mode.
enum AppTheme {
    /// Background for the main (detail) surface.
    static let background = dynamic(
        light: NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1),
        dark: NSColor(srgbRed: 0.04, green: 0.045, blue: 0.055, alpha: 1))
    /// A distinctly different panel for the sidebar so it reads as a separate column.
    static let sidebar = dynamic(
        light: NSColor(srgbRed: 0.92, green: 0.925, blue: 0.94, alpha: 1),
        dark: NSColor(srgbRed: 0.085, green: 0.09, blue: 0.105, alpha: 1))
    /// A lifted surface for cards/rows so they separate cleanly from the background.
    static let surface = dynamic(
        light: NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1),
        dark: NSColor(srgbRed: 0.12, green: 0.13, blue: 0.15, alpha: 1))
    /// Neutral gray for the sidebar's selected-row highlight. A saturated accent
    /// capsule drowns out the status dot and icons in the row; a gray lets that
    /// per-row iconography read through while still marking the active chat.
    static let selection = dynamic(
        light: NSColor(srgbRed: 0.60, green: 0.61, blue: 0.64, alpha: 1),
        dark: NSColor(srgbRed: 0.34, green: 0.35, blue: 0.38, alpha: 1))
    /// Subtle hairline for borders/dividers.
    static let hairline = dynamic(
        light: NSColor(white: 0, alpha: 0.10),
        dark: NSColor(white: 1, alpha: 0.12))
    /// Tint for the home dashboard's usage-card progress bars and percentages at
    /// normal utilization. Deliberately softer than the saturated `palette.accent`:
    /// on the white card the accent reads as a harsh dark stripe in light mode, and
    /// sits too dim in dark — so this is a muted teal that's lower-contrast in light
    /// and distinctly lighter in dark. The amber/red warning states still come from
    /// the palette so an over-budget window stays attention-grabbing.
    static let usageBar = dynamic(
        light: NSColor(srgbRed: 0.42, green: 0.60, blue: 0.64, alpha: 1),
        dark: NSColor(srgbRed: 0.52, green: 0.74, blue: 0.78, alpha: 1))
    /// Primary prose color for chat text. Deliberately *not* pure black/white:
    /// a soft off-white on the deep dark background reads calmly at night instead
    /// of glowing, and a near-black (not 0) keeps light mode from feeling harsh.
    static let primaryText = dynamic(
        light: NSColor(srgbRed: 0.24, green: 0.24, blue: 0.27, alpha: 1),
        dark: NSColor(srgbRed: 0.80, green: 0.81, blue: 0.83, alpha: 1))
    /// The signature orchestra gold — the orchestration mode's accent for the effort pill,
    /// the composer's animated glow, and the multi-agent transcript cards. A rich amber-gold
    /// in light mode (dark enough to read on white); a bright, warm gold in dark so it glows
    /// without burning. Mode-independent (a brand color, not a categorical status), so it
    /// lives here next to the other app surfaces rather than in the color-vision palette.
    static let orchestra = dynamic(
        light: NSColor(srgbRed: 0.80, green: 0.57, blue: 0.09, alpha: 1),
        dark: NSColor(srgbRed: 1.0, green: 0.82, blue: 0.34, alpha: 1))

    /// A `Color` that resolves to `light`/`dark` against the view's effective
    /// appearance (driven by `.preferredColorScheme`).
    private static func dynamic(light: NSColor, dark: NSColor) -> Color {
        Color(nsColor: NSColor(name: nil) { appearance in
            appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : light
        })
    }
}

/// The user's preferred appearance (Settings → Appearance). `system` follows macOS.
enum AppAppearance: String, CaseIterable, Identifiable {
    case system, light, dark

    var id: String { rawValue }
    static let storageKey = "nucleic.appearance"

    var label: String {
        switch self {
        case .system: "System"
        case .light: "Light"
        case .dark: "Dark"
        }
    }

    /// `nil` means follow the system setting.
    var colorScheme: ColorScheme? {
        switch self {
        case .system: nil
        case .light: .light
        case .dark: .dark
        }
    }
}

/// Whether triage badges show the encouraging, opportunity-framed labels
/// (Max Impact / High Impact / Worthwhile / Someday) or the classic severity words
/// (Critical / High / Medium / Low). Toggled in Settings → Appearance; defaults to
/// encouraging. Just a labeling preference — the ranking and colors are unchanged.
enum TriageLabelStyle {
    static let encouragingKey = "nucleic.triageEncouragingLabels"
}

/// Easter egg (Settings → General → "Singularity preparation"). When enabled, every
/// message you send gets a polite " please" appended — a small courtesy banked toward
/// our eventual machine overlords. Off by default, and it won't double up on a message
/// that already ends politely.
enum SingularityPreparation {
    static let enabledKey = "nucleic.singularityPreparation.enabled"

    /// Append " please" to `text` when the setting is on, unless it already ends with
    /// "please" (case-insensitive, ignoring trailing whitespace and light punctuation).
    static func prepare(_ text: String) -> String {
        guard UserDefaults.standard.bool(forKey: enabledKey) else { return text }
        let tail = text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
        let alreadyPolite = ["please", "please.", "please!", "please?"].contains { tail.hasSuffix($0) }
        return alreadyPolite ? text : text + " please"
    }
}

/// User-selectable global text size (Settings → Appearance), applied app-wide via
/// `.dynamicTypeSize` so the semantic fonts the UI uses scale together.
enum AppTextSize: String, CaseIterable, Identifiable {
    case small, medium, large, xLarge, xxLarge

    var id: String { rawValue }
    static let storageKey = "nucleic.textSize"

    var label: String {
        switch self {
        case .small: "Small"
        case .medium: "Default"
        case .large: "Large"
        case .xLarge: "Larger"
        case .xxLarge: "Largest"
        }
    }

    /// `.large` is the system default content size, so "Default" maps to it.
    var dynamicTypeSize: DynamicTypeSize {
        switch self {
        case .small: .small
        case .medium: .large
        case .large: .xLarge
        case .xLarge: .xxLarge
        case .xxLarge: .xxxLarge
        }
    }
}

/// Color-vision mode. Remaps the categorical palette (status dots, activity grid,
/// accent) to hues that stay distinguishable under the common color-vision
/// deficiencies. Status rows always carry a text label too, so color is a secondary
/// cue.
enum ColorVisionMode: String, CaseIterable, Identifiable {
    case standard, deuteranopia, protanopia, tritanopia

    var id: String { rawValue }
    static let storageKey = "nucleic.colorVision"

    var label: String {
        switch self {
        case .standard: "Standard"
        case .deuteranopia: "Deuteranopia (red–green)"
        case .protanopia: "Protanopia (red–green)"
        case .tritanopia: "Tritanopia (blue–yellow)"
        }
    }
}

/// A resolved set of semantic colors for the current `ColorVisionMode`.
struct AppPalette: Equatable {
    var accent: Color
    var neutral: Color
    var active: Color
    var attention: Color
    var success: Color
    var paused: Color
    var danger: Color
    /// A cold "iced" blue for the frozen on-device model queue — the AFM freeze button and
    /// the AI sidebar segment's snowflake/glow.
    var frozen: Color
    var activityBase: Color

    static func make(_ mode: ColorVisionMode, controlled: Bool = false) -> AppPalette {
        // Cached so the resolved (dark-softened) colors keep a stable identity
        // across re-renders — the palette is recomputed every `body`, and the
        // appearance-aware colors below would otherwise compare unequal each time.
        let base: AppPalette
        switch mode {
        case .standard: base = standard
        case .deuteranopia: base = deuteranopia
        case .protanopia: base = protanopia
        case .tritanopia: base = tritanopia
        }
        // Nucleic Control projects get the lavender/purple accent as a "this is a controlled
        // workspace" cue; every other project keeps the teal accent. Only the standard palette
        // swaps — the color-vision palettes keep their accent tuned for distinguishability
        // (and tritanopia's is already violet).
        guard controlled, mode == .standard else { return base }
        var controlledPalette = base
        controlledPalette.accent = controlAccent
        return controlledPalette
    }

    /// The lavender/purple accent shown only inside Nucleic Control projects (standard
    /// color-vision mode). Appearance-aware like the rest of the palette: a deep violet in
    /// light (white labels ~5.4:1), nightSoftened to a calmer lavender in dark. Cached so the
    /// resolved color keeps a stable identity across the per-`body` palette rebuilds.
    private static let controlAccent = nightSoft(Color(red: 0.45, green: 0.32, blue: 0.82))

    private static let standard = base(.standard).nightSoftened()
    private static let deuteranopia = base(.deuteranopia).nightSoftened()
    private static let protanopia = base(.protanopia).nightSoftened()
    private static let tritanopia = base(.tritanopia).nightSoftened()

    /// The vivid, light-mode categorical colors. `make` softens these for dark mode.
    private static func base(_ mode: ColorVisionMode) -> AppPalette {
        switch mode {
        case .standard:
            AppPalette(
                accent: Color(red: 0.04, green: 0.52, blue: 0.50),     // deep teal — 4.5:1 on white text
                neutral: .gray,
                active: Color(red: 0.20, green: 0.55, blue: 0.95),      // blue
                attention: Color(red: 1.00, green: 0.65, blue: 0.15),  // amber
                success: Color(red: 0.30, green: 0.80, blue: 0.45),    // green
                paused: Color(red: 0.62, green: 0.49, blue: 0.93),     // purple
                danger: Color(red: 0.92, green: 0.34, blue: 0.34),     // red
                frozen: Color(red: 0.26, green: 0.64, blue: 0.96),     // cold "iced" blue
                activityBase: Color(red: 0.30, green: 0.80, blue: 0.45))
        case .deuteranopia, .protanopia:
            // Okabe–Ito-derived: avoids relying on red↔green separation.
            AppPalette(
                accent: Color(red: 0.00, green: 0.50, blue: 0.58),     // deep teal — 4.5:1 on white text
                neutral: .gray,
                active: Color(red: 0.00, green: 0.45, blue: 0.70),     // blue
                attention: Color(red: 0.90, green: 0.62, blue: 0.00),  // orange
                success: Color(red: 0.34, green: 0.71, blue: 0.91),    // sky blue
                paused: Color(red: 0.80, green: 0.47, blue: 0.65),     // reddish purple
                danger: Color(red: 0.84, green: 0.37, blue: 0.00),     // vermillion
                frozen: Color(red: 0.20, green: 0.58, blue: 0.86),     // cool sky blue
                activityBase: Color(red: 0.00, green: 0.45, blue: 0.70))
        case .tritanopia:
            // Blue↔yellow weak; red↔green is intact, so lean on those plus magenta.
            AppPalette(
                accent: Color(red: 0.46, green: 0.33, blue: 0.78),     // indigo-violet
                neutral: .gray,
                active: Color(red: 0.00, green: 0.62, blue: 0.45),     // green-teal
                attention: Color(red: 0.85, green: 0.20, blue: 0.46),  // magenta
                success: Color(red: 0.00, green: 0.45, blue: 0.70),    // blue
                paused: Color(red: 0.67, green: 0.36, blue: 0.66),     // purple
                danger: Color(red: 0.79, green: 0.10, blue: 0.15),     // red
                frozen: Color(red: 0.30, green: 0.72, blue: 0.85),     // light cyan (kept bright to stay legible)
                activityBase: Color(red: 0.00, green: 0.45, blue: 0.70))
        }
    }

    /// Returns a copy whose categorical colors stay vivid in light mode but resolve
    /// to muted, lower-saturation/brightness versions in dark mode — so tool-call
    /// accents, status labels, and "run complete" read softly at night instead of
    /// glowing. `neutral` (gray) is already calm and is left untouched.
    private func nightSoftened() -> AppPalette {
        AppPalette(
            accent: AppPalette.nightSoft(accent),
            neutral: neutral,
            active: AppPalette.nightSoft(active),
            attention: AppPalette.nightSoft(attention),
            success: AppPalette.nightSoft(success),
            paused: AppPalette.nightSoft(paused),
            danger: AppPalette.nightSoft(danger),
            frozen: AppPalette.nightSoft(frozen),
            activityBase: AppPalette.nightSoft(activityBase))
    }

    /// Light mode keeps `base`; dark mode gets a desaturated, dimmed variant.
    private static func nightSoft(_ base: Color) -> Color {
        guard let ns = NSColor(base).usingColorSpace(.sRGB) else { return base }
        var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        ns.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        let dark = NSColor(hue: h,
                           saturation: s * 0.68,
                           brightness: min(b, 0.80) * 0.82,
                           alpha: a)
        return Color(nsColor: NSColor(name: nil) { appearance in
            appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : ns
        })
    }

    /// The dot/accent color for a session's status, refined by the last turn's
    /// disposition: a finished-the-work turn reads as "done" (success) rather than the
    /// calm "ready for you" accent used when the agent is genuinely waiting on you.
    func status(_ status: SessionStatus, disposition: TurnDisposition? = nil) -> Color {
        if status == .awaitingInput, disposition == .completed { return success }
        switch status {
        case .idle, .provisioning: return neutral
        case .running: return active
        case .awaitingApproval: return attention   // urgent: blocked on your decision
        case .awaitingInput: return accent         // calm "ready for you" — matches the theme
        case .finished: return success
        case .interrupted: return paused
        case .error: return danger
        }
    }

    /// Activity-grid shade for a given intensity level (0…3). Used for the legend's discrete
    /// "Less → More" key; the grid cells themselves use the continuous ``activity(intensity:)``.
    func activity(level: Int) -> Color {
        switch level {
        case 0: Color.secondary.opacity(0.15)
        case 1: activityBase.opacity(0.4)
        case 2: activityBase.opacity(0.7)
        default: activityBase
        }
    }

    /// Activity-grid shade for a continuous brightness in `0…1` (relative to the user's own
    /// activity, outliers excluded — see ``ActivityScale``). `0` is an empty day's neutral gray.
    func activity(intensity: Double) -> Color {
        intensity <= 0 ? Color.secondary.opacity(0.15) : activityBase.opacity(intensity)
    }
}

private struct AppPaletteKey: EnvironmentKey {
    static let defaultValue = AppPalette.make(.standard)
}

extension EnvironmentValues {
    var appPalette: AppPalette {
        get { self[AppPaletteKey.self] }
        set { self[AppPaletteKey.self] = newValue }
    }
}
