import SwiftUI
import Observation

/// Holds the live ("working") panel arrangement plus the user's saved, named
/// layouts and which one is the default. Global (app-wide), persisted to
/// `UserDefaults` as JSON — see the design note in `PaneledChatView`.
///
/// All mutation goes through here so the single working layout stays the source of
/// truth for the container view; saved layouts are immutable snapshots applied by
/// copying into `working`.
@MainActor
@Observable
final class PanelLayoutStore {
    /// The arrangement currently shown around the chat.
    private(set) var working: PanelLayout
    /// User-saved named layouts, in creation order.
    private(set) var saved: [PanelLayout]
    /// The layout applied to a fresh launch, if any.
    private(set) var defaultLayoutID: UUID?

    private let defaults: UserDefaults

    private enum Key {
        static let working = "nucleic.panels.working"
        static let saved = "nucleic.panels.saved"
        static let defaultID = "nucleic.panels.defaultID"
    }

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
        let saved = Self.decode([PanelLayout].self, from: defaults.data(forKey: Key.saved)) ?? []
        let defaultID = defaults.string(forKey: Key.defaultID).flatMap { UUID(uuidString: $0) }
        self.saved = saved
        self.defaultLayoutID = defaultID

        // Prefer the last live arrangement; otherwise fall back to the default layout,
        // otherwise an empty (chat-only) arrangement.
        if let stored = Self.decode(PanelLayout.self, from: defaults.data(forKey: Key.working)) {
            self.working = stored
        } else if let id = defaultID, let layout = saved.first(where: { $0.id == id }) {
            self.working = layout.asWorkingCopy()
        } else {
            self.working = .empty
        }
    }

    // MARK: - Queries

    var defaultLayout: PanelLayout? {
        guard let id = defaultLayoutID else { return nil }
        return saved.first { $0.id == id }
    }

    /// Which slot a placed panel currently lives in, if any.
    func slot(of instanceID: UUID) -> PanelSlot? {
        PanelSlot.allCases.first { working.instances(in: $0).contains { $0.id == instanceID } }
    }

    /// True when the working arrangement matches a saved layout's panel placement
    /// (sizes ignored) — used to show a checkmark in the layouts menu.
    func matchesWorking(_ layout: PanelLayout) -> Bool {
        func kinds(_ xs: [PanelInstance]) -> [PanelKind] { xs.map(\.kind) }
        return kinds(layout.left) == kinds(working.left)
            && kinds(layout.right) == kinds(working.right)
            && kinds(layout.bottom) == kinds(working.bottom)
    }

    // MARK: - Working arrangement mutations

    func add(_ kind: PanelKind, to slot: PanelSlot) {
        modify(slot) { $0.append(PanelInstance(kind: kind)) }
    }

    /// Surface an Editor panel in a side column in response to opening a file from the
    /// file explorer at `explorerID`. An editor already in a side column is left where
    /// it is (it already shows the file via `FileSelectionStore`); an editor docked in
    /// the bottom row is moved up to the right column; otherwise a fresh editor is
    /// added in the column opposite the file explorer.
    func revealEditor(forExplorer explorerID: UUID?) {
        if working.left.contains(where: { $0.kind == .editor })
            || working.right.contains(where: { $0.kind == .editor }) { return }

        if let bottomEditor = working.bottom.first(where: { $0.kind == .editor }) {
            drop(PanelDrag(instanceID: bottomEditor.id, kind: .editor), into: .right)
            return
        }

        let target: PanelSlot
        switch explorerID.flatMap(slot(of:)) {
        case .left: target = .right
        case .right: target = .left
        case .bottom, .none: target = .right
        }
        add(.editor, to: target)
    }

    /// Surface a Terminal panel — used by the in-chat "Log in" action so the user can
    /// complete `claude /login` without leaving the app. A terminal already placed
    /// anywhere is left where it is (it's already visible and is where the command
    /// lands); otherwise a fresh one is docked in the bottom row.
    func revealTerminal() {
        let hasTerminal = PanelSlot.allCases.contains {
            working.instances(in: $0).contains { $0.kind == .terminal }
        }
        guard !hasTerminal else { return }
        add(.terminal, to: .bottom)
    }

    func remove(_ instanceID: UUID) {
        for slot in PanelSlot.allCases {
            modify(slot) { $0.removeAll { $0.id == instanceID } }
        }
    }

    /// Move (or insert) `drag` into `slot`. A palette drag (`instanceID == nil`)
    /// inserts a new panel; an existing-panel drag is pulled from wherever it was
    /// first, then inserted, so dragging within the same slot reorders it.
    func drop(_ drag: PanelDrag, into slot: PanelSlot, at index: Int? = nil) {
        var moving = PanelInstance(kind: drag.kind)
        if let id = drag.instanceID {
            for s in PanelSlot.allCases {
                if let existing = working.instances(in: s).first(where: { $0.id == id }) {
                    moving = existing
                }
            }
            // Remove from all slots before re-inserting.
            for s in PanelSlot.allCases { modify(s) { $0.removeAll { $0.id == id } } }
        }
        modify(slot) { items in
            let i = min(max(index ?? items.count, 0), items.count)
            items.insert(moving, at: i)
        }
    }

    /// Live region-size updates during a divider drag. These deliberately do *not*
    /// persist — a drag fires many per second, and synchronously re-encoding JSON on
    /// each tick caused visible jitter (especially with a `TextEditor` in a panel).
    /// Call `commitSizes()` once on drag end to persist the final size.
    func setWidth(_ width: CGFloat, for slot: PanelSlot) {
        switch slot {
        case .left: working.leftWidth = width
        case .right: working.rightWidth = width
        case .bottom: break
        }
    }

    func setBottomHeight(_ height: CGFloat) {
        working.bottomHeight = height
    }

    /// Persist the working layout after a resize gesture finishes.
    func commitSizes() { persistWorking() }

    func clearAll() {
        working = .empty
        persistWorking()
    }

    private func modify(_ slot: PanelSlot, _ change: (inout [PanelInstance]) -> Void) {
        switch slot {
        case .left: change(&working.left)
        case .right: change(&working.right)
        case .bottom: change(&working.bottom)
        }
        persistWorking()
    }

    // MARK: - Saved layouts

    /// Snapshot the working arrangement under `name`. If a saved layout already has
    /// that name, it's overwritten in place (keeping its id, so it stays the default
    /// if it was).
    func saveWorking(as name: String) {
        let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        if let index = saved.firstIndex(where: { $0.name.caseInsensitiveCompare(trimmed) == .orderedSame }) {
            var updated = working
            updated.id = saved[index].id
            updated.name = trimmed
            saved[index] = updated
        } else {
            var snapshot = working
            snapshot.id = UUID()
            snapshot.name = trimmed
            saved.append(snapshot)
        }
        persistSaved()
    }

    func apply(_ layout: PanelLayout) {
        working = layout.asWorkingCopy()
        persistWorking()
    }

    func deleteSaved(_ id: UUID) {
        saved.removeAll { $0.id == id }
        if defaultLayoutID == id { defaultLayoutID = nil; persistDefault() }
        persistSaved()
    }

    func renameSaved(_ id: UUID, to name: String) {
        let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty, let index = saved.firstIndex(where: { $0.id == id }) else { return }
        saved[index].name = trimmed
        persistSaved()
    }

    func setDefault(_ id: UUID?) {
        defaultLayoutID = id
        persistDefault()
    }

    // MARK: - Persistence

    private func persistWorking() { defaults.set(Self.encode(working), forKey: Key.working) }
    private func persistSaved() { defaults.set(Self.encode(saved), forKey: Key.saved) }
    private func persistDefault() {
        if let id = defaultLayoutID { defaults.set(id.uuidString, forKey: Key.defaultID) }
        else { defaults.removeObject(forKey: Key.defaultID) }
    }

    private static func encode<T: Encodable>(_ value: T) -> Data? { try? JSONEncoder().encode(value) }
    private static func decode<T: Decodable>(_ type: T.Type, from data: Data?) -> T? {
        guard let data else { return nil }
        return try? JSONDecoder().decode(T.self, from: data)
    }
}
