import SwiftUI
import AppKit

/// A lazy file tree of the session's worktree. Clicking a file selects it and opens
/// it in any Editor panel (via `FileSelectionStore`); double-clicking also surfaces an
/// Editor in a side column (see `PanelLayoutStore.revealEditor`). Directories load
/// their children on first expand, so even a large repo is cheap to show.
struct FileExplorerPanel: View {
    let root: String?
    /// This file-explorer panel's own instance id, so a double-click can place the
    /// editor in the column opposite this one.
    let instanceID: UUID
    @State private var selection: FileNode?
    @Environment(FileSelectionStore.self) private var fileStore
    @Environment(PanelLayoutStore.self) private var panels

    var body: some View {
        if let root {
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 0) {
                    FileTree(
                        node: FileNode(url: URL(fileURLWithPath: root), isDirectory: true),
                        depth: 0, selection: $selection, onOpenInPane: openInPane)
                }
                .padding(.vertical, 8)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        } else {
            VStack(spacing: 8) {
                Image(systemName: "folder").font(.largeTitle).foregroundStyle(.tertiary)
                Text("No worktree yet").font(.callout).foregroundStyle(.secondary)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }

    /// Open a file in a dedicated Editor column: load it into the shared selection and
    /// make sure an Editor panel is showing in a side column to display it.
    private func openInPane(_ url: URL) {
        fileStore.open(url)
        panels.revealEditor(forExplorer: instanceID)
    }
}

/// A node in the worktree tree. Identity is the path, so SwiftUI keeps expand state
/// stable across redraws.
struct FileNode: Identifiable, Hashable {
    let url: URL
    let isDirectory: Bool
    var id: String { url.path }
    var name: String { url.lastPathComponent }
}

/// One row plus, when expanded, its children. Children are read lazily the first
/// time the row is opened.
private struct FileTree: View {
    let node: FileNode
    let depth: Int
    @Binding var selection: FileNode?
    /// Called when a file is double-clicked, to open it in an Editor column.
    let onOpenInPane: (URL) -> Void

    @State private var expanded = false
    @State private var children: [FileNode]?
    @Environment(FileSelectionStore.self) private var fileStore

    private var isSelected: Bool { selection == node }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            row
            if expanded, let children {
                ForEach(children) { child in
                    FileTree(node: child, depth: depth + 1,
                             selection: $selection, onOpenInPane: onOpenInPane)
                }
            }
        }
        .onAppear {
            // The root folder opens expanded so the tree shows content immediately
            // instead of a single collapsed row.
            if depth == 0, node.isDirectory, children == nil {
                children = Self.loadChildren(of: node.url)
                expanded = true
            }
        }
    }

    private var row: some View {
        Button(action: activate) {
            HStack(spacing: 6) {
                Image(systemName: "chevron.right")
                    .font(.system(size: 11, weight: .semibold))
                    .foregroundStyle(.tertiary)
                    .rotationEffect(.degrees(expanded ? 90 : 0))
                    .opacity(node.isDirectory ? 1 : 0)
                Image(systemName: node.isDirectory ? "folder" : icon(for: node.name))
                    .font(.system(size: 13))
                    .foregroundStyle(.secondary)
                    .frame(width: 18)
                Text(node.name).lineLimit(1)
                Spacer(minLength: 0)
            }
            // The chat pane's prose size, so the tree reads as part of the same
            // interface rather than a denser utility panel.
            .font(.system(size: 15))
            .padding(.vertical, 4)
            .padding(.leading, CGFloat(depth) * 16 + 10)
            .padding(.trailing, 8)
            .contentShape(Rectangle())
            .background {
                if isSelected {
                    RoundedRectangle(cornerRadius: 6, style: .continuous)
                        .fill(AppTheme.selection)
                        .padding(.horizontal, 4)
                        .padding(.vertical, 1)
                }
            }
        }
        .buttonStyle(.plain)
        // Double-clicking a file opens it in a dedicated Editor column; the button's
        // single-click selection still fires first, so a single click keeps just
        // selecting/previewing. Directories ignore the double-click (the single click
        // already toggles them).
        .simultaneousGesture(TapGesture(count: 2).onEnded {
            if !node.isDirectory { onOpenInPane(node.url) }
        })
    }

    private func activate() {
        if node.isDirectory {
            if children == nil { children = Self.loadChildren(of: node.url) }
            withAnimation(.easeOut(duration: 0.12)) { expanded.toggle() }
        } else {
            selection = node
            // Publish to the shared selection so an Editor panel opens this file too.
            fileStore.open(node.url)
        }
    }

    /// Directories first, then files, each alphabetical (case-insensitive). Hidden
    /// `.`-files are kept (a worktree's `.gitignore` etc. are worth seeing) but the
    /// heavyweight `.git` directory is skipped.
    static func loadChildren(of url: URL) -> [FileNode] {
        let keys: [URLResourceKey] = [.isDirectoryKey]
        guard let contents = try? FileManager.default.contentsOfDirectory(
            at: url, includingPropertiesForKeys: keys,
            options: [.skipsPackageDescendants]) else { return [] }
        return contents
            .filter { $0.lastPathComponent != ".git" }
            .map { FileNode(url: $0, isDirectory: (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false) }
            .sorted { a, b in
                if a.isDirectory != b.isDirectory { return a.isDirectory }
                return a.name.localizedCaseInsensitiveCompare(b.name) == .orderedAscending
            }
    }

    private func icon(for name: String) -> String {
        switch (name as NSString).pathExtension.lowercased() {
        case "swift": "swift"
        case "md", "markdown", "txt": "doc.text"
        case "json", "yml", "yaml", "toml", "plist": "curlybraces"
        case "png", "jpg", "jpeg", "gif", "svg", "heic": "photo"
        case "sh", "zsh", "bash": "terminal"
        default: "doc"
        }
    }
}
