import AVKit
import SwiftUI
import UniformTypeIdentifiers

struct ContentView: View {
    @StateObject private var processor = VideoProcessor()
    @State private var settings = SettingsStore.load()
    @State private var selectedClipID: UUID?
    @State private var variantPickerClip: GrowthLapseProjectClip?
    @State private var reviewPanelHeight: CGFloat = 340

    var body: some View {
        HSplitView {
            Form {
                Section("Dateien") {
                    Picker("Modus", selection: $settings.renderMode) {
                        ForEach(RenderMode.allCases) { mode in
                            Text(mode.rawValue).tag(mode)
                        }
                    }
                    .help("Video-Zeitraffer verarbeitet MOV/MP4-Clips. Foto-Morphing nutzt Bilder aus dem Input-Ordner oder extrahiert passende Standbilder aus Videos und erzeugt Landmark-basierte Morph-Übergänge.")

                    pickerRow(
                        title: "Input-Ordner",
                        value: settings.inputFolder?.path(percentEncoded: false) ?? "Nicht gewählt",
                        buttonTitle: "Auswählen",
                        help: settings.renderMode == .photoMorphing
                            ? "Ordner mit chronologisch sortierten Bildern oder Videos. Wenn keine Bilder gefunden werden, extrahiert GrowthLapse Standbilder aus den Videos."
                            : "Ordner mit den chronologisch sortierten Quellvideos. Die gewählte Output-Datei wird automatisch aus der Input-Liste ausgeschlossen."
                    ) {
                        selectInputFolder()
                    }

                    pickerRow(
                        title: "Output-Datei",
                        value: settings.outputFile?.path(percentEncoded: false) ?? "Nicht gewählt",
                        buttonTitle: "Speichern unter",
                        help: "Ziel-MP4. Neben dieser Datei legt GrowthLapse ein Render-Protokoll mit den ausgeführten Schritten an."
                    ) {
                        selectOutputFile()
                    }

                    Button("Projekt laden") {
                        selectProjectFile()
                    }
                    .help("Lädt eine gespeicherte .growthlapse-project.json, inklusive Segmentfenstern und Variantenwahl.")
                }
                .disabled(processor.state.isRunning)

                Section("FFmpeg") {
                    pickerRow(
                        title: "ffmpeg",
                        value: settings.ffmpegExecutable?.path(percentEncoded: false) ?? "Automatisch suchen",
                        buttonTitle: "Auswählen",
                        help: "Optionaler Pfad zu einer eigenen ffmpeg-Binary, z.B. mit libvidstab. Leer lassen für automatische Suche."
                    ) {
                        selectExecutable(title: "ffmpeg auswählen") { url in
                            settings.ffmpegExecutable = url
                        }
                    }

                    pickerRow(
                        title: "ffprobe",
                        value: settings.ffprobeExecutable?.path(percentEncoded: false) ?? "Automatisch suchen",
                        buttonTitle: "Auswählen",
                        help: "Optionaler Pfad zu ffprobe passend zu deiner ffmpeg-Version. ffprobe liest Dauer und Videoabmessungen."
                    ) {
                        selectExecutable(title: "ffprobe auswählen") { url in
                            settings.ffprobeExecutable = url
                        }
                    }

                    Button("Automatische Suche verwenden") {
                        settings.ffmpegExecutable = nil
                        settings.ffprobeExecutable = nil
                    }
                    .disabled(settings.ffmpegExecutable == nil && settings.ffprobeExecutable == nil)
                }
                .disabled(processor.state.isRunning)

                Section("Parameter") {
                    if settings.renderMode == .videoGrowthLapse {
                        doubleStepper("Ausschnittlänge", value: $settings.segmentLength, range: 1...120, step: 1, suffix: "s", help: "So viele Sekunden werden aus jedem Originalvideo entnommen, bevor Stabilisierung und Beschleunigung passieren.")
                    }
                    doubleStepper("Ziellänge pro Clip", value: $settings.targetClipLength, range: 0.5...30, step: 0.5, suffix: "s", help: settings.renderMode == .photoMorphing ? "Dauer des Morph-Übergangs von einem Bild zum nächsten." : "Länge jedes fertigen Zwischenclips nach der Beschleunigung. Beispiel: 10 Sekunden Segment auf 2 Sekunden ergibt 5x Speed.")
                    doubleStepper("Übergangslänge", value: $settings.transitionLength, range: 0...5, step: 0.1, suffix: "s", help: "Dauer des Crossfade zwischen zwei Clips. 0 verbindet hart per concat.")
                    Stepper(value: $settings.fps, in: 1...120) {
                        HStack {
                            Text("FPS")
                            Spacer()
                            Text("\(settings.fps)")
                                .foregroundStyle(.secondary)
                        }
                    }
                    .help("Bildrate der normalisierten Zwischenclips und des finalen Videos. Für TV sind 30 fps meistens passend.")
                    Stepper(value: $settings.crf, in: 0...51) {
                        HStack {
                            Text("CRF")
                            Spacer()
                            Text("\(settings.crf)")
                                .foregroundStyle(.secondary)
                        }
                    }
                    .help("Qualitätswert für libx264. Niedriger ist bessere Qualität/größere Datei. Wird bei VideoToolbox nicht verwendet.")
                    if settings.renderMode == .videoGrowthLapse {
                        doubleStepper("Start-Offset", value: $settings.startOffset, range: -60...60, step: 0.5, suffix: "s", help: "Verschiebt den gewählten Ausschnitt relativ zur Mitte. Positive Werte nehmen späteres Material, negative früheres.")
                    }
                }
                .disabled(processor.state.isRunning)

                Section("Ausgabe") {
                    Picker("Format", selection: $settings.outputFormat) {
                        ForEach(OutputFormat.allCases) { format in
                            Text(format.rawValue).tag(format)
                        }
                    }
                    .help("Zielbildformat. Für TV ist Landscape 1920x1080 SDR die robuste Wahl.")

                    Picker("Encoder", selection: $settings.videoEncoder) {
                        ForEach(VideoEncoder.allCases) { encoder in
                            Text(encoder.rawValue).tag(encoder)
                        }
                    }
                    .help("VideoToolbox nutzt Apples Hardware-Encoder. libx264 ist CPU-lastiger, aber klassisch kontrollierbar über CRF.")

                    Picker("Sortierung", selection: $settings.sortMode) {
                        ForEach(SortMode.allCases) { sortMode in
                            Text(sortMode.rawValue).tag(sortMode)
                        }
                    }
                    .help("Bestimmt die chronologische Reihenfolge der Quellvideos. Wenn Dateinamen wie '118 Wochen alt' enthalten sind, nutze 'Alter in Wochen aufsteigend'.")

                    Picker("Mehrfach-Versionen", selection: $settings.duplicateClipSelection) {
                        ForEach(DuplicateClipSelection.allCases) { selection in
                            Text(selection.rawValue).tag(selection)
                        }
                    }
                    .help("Wenn mehrere Dateien denselben Namen mit Endung 01, 02, 03 haben, wählt GrowthLapse nur eine Variante. Beispiel: '... 118 Wochen alt 01.MOV' und '... 118 Wochen alt 02.MOV'.")

                    if settings.videoEncoder == .videoToolbox {
                        Stepper(value: $settings.hardwareBitrateMbps, in: 2...80) {
                            HStack {
                                Text("Hardware-Bitrate")
                                Spacer()
                                Text("\(settings.hardwareBitrateMbps) Mbit/s")
                                    .foregroundStyle(.secondary)
                            }
                        }
                        .help("Bitrate für Hardware-Encoding mit VideoToolbox. Höher bedeutet bessere Qualität und größere Dateien.")
                    }

                    Toggle("Audio entfernen", isOn: $settings.removeAudio)
                        .help("Entfernt Tonspuren aus Zwischenclips und finalem Video. Für GrowthLapse normalerweise empfohlen.")
                    Toggle("Segment aus der Mitte nehmen", isOn: $settings.takeMiddleSegment)
                        .help("Wählt den Ausschnitt aus der Videomitte. Start-Offset verschiebt diesen Mittelpunkt.")
                    Toggle("Zwischenclips behalten", isOn: $settings.keepIntermediateClips)
                        .help("Behält segment_*, stabilized_*, clip_* und xfade_* Dateien zur Kontrolle. Sonst wird der Temp-Ordner nach Erfolg gelöscht.")
                    Toggle("Render-Cache für Nachbearbeitung behalten", isOn: $settings.keepRenderCacheForReview)
                        .help("Behält die normalisierten clip_*.mp4 Dateien und eine Projektdatei neben dem Output. So können einzelne Segmentfenster später angepasst und schneller neu gerendert werden.")
                    Toggle("Clipnummer einblenden", isOn: $settings.burnInClipNumbers)
                        .help("Brennt die laufende Clipnummer unten links ins Video ein. Praktisch, um im fertigen Film problematische Clips schnell in der Nachbearbeitung zu finden.")
                }
                .disabled(processor.state.isRunning)

                Section("Stabilisierung") {
                    Toggle("Stabilisierung aktivieren", isOn: $settings.stabilizationEnabled)
                        .help("Stabilisiert globale Kamerabewegung. Das ist getrennt von Face Size Normalization und folgt nicht gezielt Gesichtern.")

                    Picker("Methode", selection: $settings.stabilizationMethod) {
                        ForEach(StabilizationMethod.allCases) { method in
                            Text(method.rawValue).tag(method)
                        }
                    }
                    .help("VidStab ist bevorzugt und nutzt Analyse plus Transform. Deshake ist schnellerer Fallback ohne getrennten Analyse-Pass.")
                    .disabled(!settings.stabilizationEnabled)

                    Picker("Stärke", selection: $settings.stabilizationStrength) {
                        ForEach(StabilizationStrength.allCases) { strength in
                            Text(strength.rawValue).tag(strength)
                        }
                    }
                    .help("Stärkere Werte glätten mehr Bewegung, können aber stärker zoomen/croppen oder schwimmender wirken.")
                    .disabled(!settings.stabilizationEnabled)

                    Toggle("Auf Hintergrundpunkt fokussieren", isOn: $settings.stabilizationAnchorEnabled)
                        .help("Beschränkt nur die VidStab-Analyse auf einen Bereich um einen festen Hintergrundpunkt. Der Transform bleibt auf dem vollen Bild.")
                        .disabled(!settings.stabilizationEnabled || settings.stabilizationMethod != .vidstab)

                    if settings.stabilizationAnchorEnabled {
                        percentSlider("Punkt X", value: $settings.stabilizationAnchorX, range: 0...1, help: "Horizontale Position des festen Hintergrundpunkts im Bild. 0% links, 100% rechts.")
                        percentSlider("Punkt Y", value: $settings.stabilizationAnchorY, range: 0...1, help: "Vertikale Position des festen Hintergrundpunkts im Bild. 0% oben, 100% unten.")
                        percentSlider("Analysebereich", value: $settings.stabilizationAnchorSize, range: 0.15...0.80, help: "Größe des Bereichs um den Hintergrundpunkt, den VidStab zur Bewegungsanalyse verwendet.")
                    }
                }
                .disabled(processor.state.isRunning)

                Section("Gesicht") {
                    Toggle("Bestes Segment automatisch finden", isOn: $settings.bestSegmentDetectionEnabled)
                        .help("Analysiert das Originalvideo vor dem Schneiden und sucht das Fenster, in dem Gesicht und beide Augen am besten sichtbar sind. Wenn nichts Brauchbares gefunden wird, nutzt GrowthLapse die normale Mitte/Start-Offset-Logik.")

                    Toggle("Gesichtsgröße normalisieren", isOn: $settings.faceNormalizationEnabled)
                        .help("Analysiert mehrere Frames mit Apple Vision und berechnet einen statischen Crop pro Clip. Kein Frame-by-frame Tracking.")

                    HStack {
                        Text("Ziel-Gesichtshöhe")
                        Spacer()
                        Text("\(Int(settings.targetFaceHeightRatio * 100)) %")
                            .foregroundStyle(.secondary)
                    }
                    .help("Angestrebte Gesichtshöhe relativ zur Ausgabehöhe. 28% ist ein guter Startwert für Portrait-/GrowthLapse-Material.")

                    Slider(value: $settings.targetFaceHeightRatio, in: 0.18...0.40, step: 0.01)
                        .disabled(!settings.faceNormalizationEnabled)
                }
                .disabled(processor.state.isRunning)

                Section {
                    HStack {
                        Button("Render starten") {
                            processor.start(settings: settings)
                        }
                        .help("Startet die komplette Pipeline: Segment, Stabilisierung, Face-Crop, Normalisierung und Übergänge.")
                        .keyboardShortcut(.return, modifiers: [.command])
                        .disabled(processor.state.isRunning)

                        Button("STOP") {
                            processor.cancel()
                        }
                        .help("Bricht den laufenden ffmpeg-Prozess ab und räumt temporäre Dateien auf.")
                        .tint(.red)
                        .disabled(!processor.state.isRunning)

                        Button("Defaults") {
                            settings = SettingsStore.reset()
                        }
                        .help("Setzt alle Einstellungen und gespeicherten Pfade auf Werkseinstellungen zurück.")
                        .disabled(processor.state.isRunning)
                    }

                    if case .finished(let url) = processor.state {
                        Button("Im Finder anzeigen") {
                            processor.showInFinder(url: url)
                        }
                        .help("Zeigt die fertige Output-Datei im Finder an.")
                    }
                }
            }
            .formStyle(.grouped)
            .frame(minWidth: 390, idealWidth: 430, maxWidth: 500)
            .padding()

            VStack(alignment: .leading, spacing: 14) {
                statusHeader
                ProgressView(value: processor.progress)
                    .progressViewStyle(.linear)

                if let project = processor.project {
                    ResizableVerticalStack(
                        topHeight: $reviewPanelHeight,
                        minTopHeight: 260,
                        minBottomHeight: 160
                    ) {
                        projectReviewPanel(project)
                    } bottom: {
                        logView
                    }
                }
                else {
                    logView
                }
            }
            .padding()
            .frame(minWidth: 520)
        }
        .alert("GrowthLapse", isPresented: errorBinding) {
            Button("OK", role: .cancel) {}
        } message: {
            if case .failed(let message) = processor.state {
                Text(message)
            }
        }
        .onChange(of: settings) { newSettings in
            SettingsStore.save(newSettings)
        }
        .onChange(of: processor.project) { project in
            guard let project else {
                selectedClipID = nil
                return
            }
            let orderedClips = orderedProjectClips(project.clips)
            if selectedClipID == nil || !orderedClips.contains(where: { $0.id == selectedClipID }) {
                selectedClipID = orderedClips.first?.id
            }
        }
        .onChange(of: settings.sortMode) { sortMode in
            processor.reorderProject(sortMode: sortMode)
        }
        .sheet(item: $variantPickerClip) { clip in
            ClipVariantPickerView(clip: currentClip(id: clip.id) ?? clip) { variant in
                processor.switchClip(clip, to: variant)
                variantPickerClip = nil
            }
            .frame(minWidth: 560, minHeight: 420)
        }
    }

    private var statusHeader: some View {
        VStack(alignment: .leading, spacing: 6) {
            HStack {
                Text("Render-Status")
                    .font(.title2.bold())
                Spacer()
                Text("\(Int(processor.progress * 100)) %")
                    .font(.headline.monospacedDigit())
                    .foregroundStyle(.secondary)
            }

            Text(processor.progressText)
                .foregroundStyle(.secondary)

            if let warning = processor.warningText {
                Text(warning)
                    .font(.callout)
                    .foregroundStyle(.orange)
                    .fixedSize(horizontal: false, vertical: true)
            }
        }
    }

    private var logView: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 4) {
                    ForEach(Array(processor.logs.enumerated()), id: \.offset) { index, line in
                        Text(line)
                            .font(.system(.caption, design: .monospaced))
                            .textSelection(.enabled)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .id(index)
                    }
                }
                .padding(10)
            }
            .background(Color(nsColor: .textBackgroundColor))
            .clipShape(RoundedRectangle(cornerRadius: 8))
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(Color(nsColor: .separatorColor))
            )
            .onChange(of: processor.logs.count) { count in
                if count > 0 {
                    proxy.scrollTo(count - 1, anchor: .bottom)
                }
            }
        }
    }

    private func projectReviewPanel(_ project: GrowthLapseProject) -> some View {
        let orderedClips = orderedProjectClips(project.clips)
        let selectedClip = orderedClips.first { $0.id == selectedClipID } ?? orderedClips.first

        return VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text("Clip-Nachbearbeitung")
                    .font(.headline)
                Spacer()
                Button("Änderungen neu rendern") {
                    processor.rerenderProject(settings: settings)
                }
                .disabled(processor.state.isRunning || !project.clips.contains(where: \.needsRender))
                .help("Rendert nur geänderte normalisierte Clips neu und baut danach das finale Video erneut.")

                Button("Projekt speichern") {
                    processor.saveCurrentProject()
                }
                .disabled(processor.state.isRunning)
                .help("Speichert die aktuelle Nachbearbeitung in die .growthlapse-project.json neben der Output-Datei. Segmentänderungen werden zusätzlich automatisch gespeichert.")

                Button("Ergebnis bestätigen") {
                    processor.confirmProjectAndDeleteCache()
                }
                .disabled(processor.state.isRunning)
                .help("Löscht den Render-Cache. Die Projektdatei und das finale Video bleiben erhalten.")
            }

            HStack(alignment: .top, spacing: 12) {
                List(orderedClips, selection: $selectedClipID) { clip in
                    VStack(alignment: .leading, spacing: 2) {
                        HStack {
                            Text(String(format: "%03d", clip.index))
                                .font(.caption.monospacedDigit())
                                .foregroundStyle(.secondary)
                            if clip.availableVariants.count > 1 {
                                Text("v\(clip.availableVariants.count)")
                                    .font(.caption.bold())
                                    .foregroundStyle(.white)
                                    .padding(.horizontal, 5)
                                    .padding(.vertical, 1)
                                    .background(Color.accentColor)
                                    .clipShape(RoundedRectangle(cornerRadius: 4))
                                    .help("Mehrere Versionen verfügbar. Doppelklick zum Wechseln.")
                            }
                            Text(clip.displayName)
                                .lineLimit(1)
                            if clip.needsRender {
                                Text("geändert")
                                    .font(.caption)
                                    .foregroundStyle(.orange)
                            }
                        }
                        Text("Start \(formattedNumber(clip.segmentStart)) s, Länge \(formattedNumber(clip.segmentLength)) s")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    .contentShape(Rectangle())
                    .onTapGesture {
                        selectedClipID = clip.id
                    }
                    .onTapGesture(count: 2) {
                        selectedClipID = clip.id
                        if clip.availableVariants.count > 1 {
                            variantPickerClip = clip
                        }
                    }
                }
                .frame(minWidth: 240, idealWidth: 300, maxWidth: 340, maxHeight: .infinity)

                if let clip = selectedClip {
                    clipEditor(clip)
                } else {
                    Text("Kein Clip ausgewählt")
                        .foregroundStyle(.secondary)
                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
                }
            }
            .frame(maxHeight: .infinity, alignment: .top)
        }
        .padding(10)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .background(Color(nsColor: .controlBackgroundColor))
        .clipShape(RoundedRectangle(cornerRadius: 8))
        .overlay(
            RoundedRectangle(cornerRadius: 8)
                .stroke(Color(nsColor: .separatorColor))
        )
    }

    private func clipEditor(_ clip: GrowthLapseProjectClip) -> some View {
        let liveClip = currentClip(id: clip.id) ?? clip
        let maxStart = max(0, liveClip.duration - liveClip.segmentLength)
        let startBinding = Binding<Double>(
            get: { currentClip(id: clip.id)?.segmentStart ?? clip.segmentStart },
            set: { processor.updateClip(clip, segmentStart: $0) }
        )

        return GeometryReader { proxy in
            let previewHeight = min(max(150, proxy.size.height - 160), 420)

            VStack(alignment: .leading, spacing: 8) {
                ClipPreviewView(clip: liveClip)
                    .id(liveClip.sourcePath)
                    .frame(height: previewHeight)
                    .clipShape(RoundedRectangle(cornerRadius: 6))

                Text(liveClip.displayName)
                    .font(.subheadline.weight(.semibold))
                    .lineLimit(2)

                HStack {
                    Text("Segmentstart")
                    Spacer()
                    Text("\(formattedNumber(startBinding.wrappedValue)) s / max \(formattedNumber(maxStart)) s")
                        .font(.body.monospacedDigit())
                        .foregroundStyle(.secondary)
                }

                SegmentTimelineView(clip: liveClip)
                    .frame(height: 42)
                    .help("Blau ist das verwendete Segmentfenster. Der markierte rechte Bereich kann nicht als Startpunkt gewählt werden, weil das Segment sonst über das Videoende hinauslaufen würde.")

                if maxStart > 0 {
                    Slider(value: startBinding, in: 0...maxStart, step: 0.1)
                } else {
                    Slider(value: .constant(0), in: 0...1)
                        .disabled(true)
                }

                HStack {
                    Button("-1 s") {
                        processor.updateClip(clip, segmentStart: startBinding.wrappedValue - 1)
                    }
                    Button("-0,5 s") {
                        processor.updateClip(clip, segmentStart: startBinding.wrappedValue - 0.5)
                    }
                    Button("Zur Mitte") {
                        processor.updateClip(clip, segmentStart: max(0, (liveClip.duration - liveClip.segmentLength) / 2 + settings.startOffset))
                    }
                    Button("+0,5 s") {
                        processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 0.5)
                    }
                    Button("+1 s") {
                        processor.updateClip(clip, segmentStart: startBinding.wrappedValue + 1)
                    }
                }
                .disabled(processor.state.isRunning || maxStart <= 0)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }

    private func currentClip(id: UUID) -> GrowthLapseProjectClip? {
        processor.project?.clips.first { $0.id == id }
    }

    private func orderedProjectClips(_ clips: [GrowthLapseProjectClip]) -> [GrowthLapseProjectClip] {
        clips.sorted { left, right in
            switch settings.sortMode {
            case .filenameAscending:
                return left.displayName.localizedStandardCompare(right.displayName) == .orderedAscending
            case .ageWeeksAscending:
                if left.ageWeeks != right.ageWeeks {
                    return (left.ageWeeks ?? Int.max) < (right.ageWeeks ?? Int.max)
                }
                return left.displayName.localizedStandardCompare(right.displayName) == .orderedAscending
            case .modificationDateAscending:
                return left.index < right.index
            }
        }
        .enumerated()
        .map { offset, clip in
            var updated = clip
            updated.index = offset + 1
            return updated
        }
    }

    private var errorBinding: Binding<Bool> {
        Binding(
            get: {
                if case .failed = processor.state {
                    return true
                }
                return false
            },
            set: { newValue in
                if !newValue, case .failed = processor.state {
                    processor.state = .idle
                }
            }
        )
    }

    private func pickerRow(title: String, value: String, buttonTitle: String, help: String? = nil, action: @escaping () -> Void) -> some View {
        VStack(alignment: .leading, spacing: 6) {
            HStack {
                Text(title)
                Spacer()
                Button(buttonTitle, action: action)
            }
            Text(value)
                .font(.caption)
                .foregroundStyle(.secondary)
                .lineLimit(2)
                .truncationMode(.middle)
        }
        .help(help ?? "")
    }

    private func doubleStepper(
        _ title: String,
        value: Binding<Double>,
        range: ClosedRange<Double>,
        step: Double,
        suffix: String,
        help: String
    ) -> some View {
        Stepper(value: value, in: range, step: step) {
            HStack {
                Text(title)
                Spacer()
                Text("\(formattedNumber(value.wrappedValue)) \(suffix)")
                    .font(.body.monospacedDigit())
                    .foregroundStyle(.secondary)
            }
        }
        .help(help)
    }

    private func percentSlider(_ title: String, value: Binding<Double>, range: ClosedRange<Double>, help: String) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack {
                Text(title)
                Spacer()
                Text("\(Int(value.wrappedValue * 100)) %")
                    .font(.body.monospacedDigit())
                    .foregroundStyle(.secondary)
            }
            Slider(value: value, in: range, step: 0.01)
        }
        .help(help)
    }

    private func formattedNumber(_ value: Double) -> String {
        let formatter = NumberFormatter()
        formatter.locale = .current
        formatter.minimumFractionDigits = value.rounded() == value ? 0 : 1
        formatter.maximumFractionDigits = 2
        return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
    }

    private func selectInputFolder() {
        let panel = NSOpenPanel()
        panel.title = "Input-Ordner auswählen"
        panel.canChooseFiles = false
        panel.canChooseDirectories = true
        panel.allowsMultipleSelection = false
        if panel.runModal() == .OK {
            settings.inputFolder = panel.url
        }
    }

    private func selectOutputFile() {
        let panel = NSSavePanel()
        panel.title = "Output-Datei auswählen"
        panel.allowedContentTypes = [.mpeg4Movie]
        panel.nameFieldStringValue = settings.outputFile?.lastPathComponent ?? "Joshua_GrowthLapse.mp4"
        if panel.runModal() == .OK, let url = panel.url {
            settings.outputFile = url.pathExtension.isEmpty ? url.appendingPathExtension("mp4") : url
        }
    }

    private func selectProjectFile() {
        let panel = NSOpenPanel()
        panel.title = "GrowthLapse-Projekt laden"
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = false
        panel.allowedContentTypes = [.json]
        panel.directoryURL = settings.outputFile?.deletingLastPathComponent()
        if panel.runModal() == .OK,
           let url = panel.url,
           let project = processor.loadProject(from: url) {
            settings.outputFile = project.outputFileURL
            settings.inputFolder = project.clips.first?.sourceURL.deletingLastPathComponent()
            selectedClipID = project.clips.first?.id
        }
    }

    private func selectExecutable(title: String, onSelect: (URL) -> Void) {
        let panel = NSOpenPanel()
        panel.title = title
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = false
        panel.treatsFilePackagesAsDirectories = true
        if panel.runModal() == .OK, let url = panel.url {
            onSelect(url)
        }
    }
}

private struct ClipPreviewView: View {
    let clip: GrowthLapseProjectClip
    @State private var player = AVPlayer()

    var body: some View {
        VideoPlayer(player: player)
            .onAppear {
                loadClip()
            }
            .onChange(of: clip.id) { _ in
                loadClip()
            }
            .onChange(of: clip.sourcePath) { _ in
                loadClip()
            }
            .onChange(of: clip.segmentStart) { _ in
                seekToSegmentStart()
            }
    }

    private func loadClip() {
        player.replaceCurrentItem(with: AVPlayerItem(url: clip.sourceURL))
        seekToSegmentStart()
    }

    private func seekToSegmentStart() {
        let time = CMTime(seconds: clip.segmentStart, preferredTimescale: 600)
        player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
    }
}

private struct ResizableVerticalStack<Top: View, Bottom: View>: View {
    @Binding var topHeight: CGFloat
    let minTopHeight: CGFloat
    let minBottomHeight: CGFloat
    @ViewBuilder let top: () -> Top
    @ViewBuilder let bottom: () -> Bottom

    @State private var dragStartHeight: CGFloat?

    var body: some View {
        GeometryReader { proxy in
            let dividerHeight: CGFloat = 14
            let availableHeight = max(0, proxy.size.height - dividerHeight)
            let maxTopHeight = max(minTopHeight, availableHeight - minBottomHeight)
            let clampedTopHeight = min(max(topHeight, minTopHeight), maxTopHeight)

            VStack(spacing: 0) {
                top()
                    .frame(height: clampedTopHeight, alignment: .top)
                    .clipped()

                SplitHandle()
                    .frame(height: dividerHeight)
                    .contentShape(Rectangle())
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged { value in
                                if dragStartHeight == nil {
                                    dragStartHeight = clampedTopHeight
                                }
                                let proposed = (dragStartHeight ?? clampedTopHeight) + value.translation.height
                                topHeight = min(max(proposed, minTopHeight), maxTopHeight)
                            }
                            .onEnded { _ in
                                dragStartHeight = nil
                            }
                    )
                    .help("Ziehen, um Clip-Nachbearbeitung und Log größer oder kleiner zu machen.")

                bottom()
                    .frame(height: max(minBottomHeight, availableHeight - clampedTopHeight), alignment: .top)
                    .clipped()
            }
            .onAppear {
                topHeight = clampedTopHeight
            }
            .onChange(of: proxy.size.height) { _ in
                topHeight = clampedTopHeight
            }
        }
    }
}

private struct SplitHandle: View {
    @State private var isHovering = false

    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color(nsColor: .windowBackgroundColor))

            RoundedRectangle(cornerRadius: 3)
                .fill(isHovering ? Color.accentColor.opacity(0.75) : Color(nsColor: .separatorColor))
                .frame(width: 84, height: 5)
        }
        .overlay(alignment: .top) {
            Rectangle()
                .fill(Color(nsColor: .separatorColor).opacity(0.45))
                .frame(height: 1)
        }
        .overlay(alignment: .bottom) {
            Rectangle()
                .fill(Color(nsColor: .separatorColor).opacity(0.45))
                .frame(height: 1)
        }
        .onHover { hovering in
            isHovering = hovering
            if hovering {
                NSCursor.resizeUpDown.push()
            } else {
                NSCursor.pop()
            }
        }
    }
}

private struct SegmentTimelineView: View {
    let clip: GrowthLapseProjectClip

    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            GeometryReader { proxy in
                let width = max(1, proxy.size.width)
                let duration = max(0.001, clip.duration)
                let maxStart = max(0, clip.duration - clip.segmentLength)
                let startX = width * CGFloat(clamp(clip.segmentStart / duration))
                let segmentWidth = max(3, width * CGFloat(clamp(clip.segmentLength / duration)))
                let invalidStartX = width * CGFloat(clamp(maxStart / duration))
                let invalidWidth = max(0, width - invalidStartX)

                ZStack(alignment: .leading) {
                    RoundedRectangle(cornerRadius: 4)
                        .fill(Color(nsColor: .separatorColor).opacity(0.35))

                    Rectangle()
                        .fill(Color.orange.opacity(0.16))
                        .frame(width: invalidWidth)
                        .offset(x: invalidStartX)

                    RoundedRectangle(cornerRadius: 4)
                        .fill(Color.accentColor.opacity(0.75))
                        .frame(width: min(segmentWidth, width - startX))
                        .offset(x: startX)

                    Rectangle()
                        .fill(Color.accentColor)
                        .frame(width: 2)
                        .offset(x: startX)
                }
            }
            .frame(height: 14)

            HStack {
                Text("0 s")
                Spacer()
                Text("Segment \(format(clip.segmentStart))-\(format(min(clip.duration, clip.segmentStart + clip.segmentLength))) s")
                Spacer()
                Text("\(format(clip.duration)) s")
            }
            .font(.caption2.monospacedDigit())
            .foregroundStyle(.secondary)
        }
    }

    private func clamp(_ value: Double) -> Double {
        min(1, max(0, value))
    }

    private func format(_ value: Double) -> String {
        let formatter = NumberFormatter()
        formatter.locale = .current
        formatter.minimumFractionDigits = value.rounded() == value ? 0 : 1
        formatter.maximumFractionDigits = 1
        return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
    }
}

private struct ClipVariantPickerView: View {
    let clip: GrowthLapseProjectClip
    let onSelect: (GrowthLapseClipVariant) -> Void
    @Environment(\.dismiss) private var dismiss
    @State private var selectedVariantID: UUID?

    private var selectedVariant: GrowthLapseClipVariant? {
        let variants = clip.availableVariants
        if let selectedVariantID,
           let variant = variants.first(where: { $0.id == selectedVariantID }) {
            return variant
        }
        return variants.first(where: { $0.sourcePath == clip.sourcePath }) ?? variants.first
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("Clip-Variante wählen")
                    .font(.title3.bold())
                Spacer()
                Button("Schließen") {
                    dismiss()
                }
            }

            if let selectedVariant {
                VariantPreviewView(variant: selectedVariant)
                    .id(selectedVariant.sourcePath)
                    .frame(height: 220)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            }

            List(clip.availableVariants, selection: $selectedVariantID) { variant in
                HStack {
                    VStack(alignment: .leading, spacing: 3) {
                        Text(variant.displayName)
                            .lineLimit(1)
                        Text("\(format(variant.duration)) s, \(variant.width)x\(variant.height)")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    Spacer()
                    if variant.sourcePath == clip.sourcePath {
                        Text("aktuell")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    Button("Verwenden") {
                        onSelect(variant)
                    }
                    .disabled(variant.sourcePath == clip.sourcePath)
                }
                .contentShape(Rectangle())
                .onTapGesture {
                    selectedVariantID = variant.id
                }
                .onTapGesture(count: 2) {
                    if variant.sourcePath != clip.sourcePath {
                        onSelect(variant)
                    }
                }
            }
        }
        .padding()
        .onAppear {
            selectedVariantID = currentVariantID()
        }
        .onChange(of: clip.id) { _ in
            selectedVariantID = currentVariantID()
        }
        .onChange(of: clip.sourcePath) { _ in
            selectedVariantID = currentVariantID()
        }
    }

    private func currentVariantID() -> UUID? {
        clip.availableVariants.first(where: { $0.sourcePath == clip.sourcePath })?.id
            ?? clip.availableVariants.first?.id
    }

    private func format(_ value: Double) -> String {
        let formatter = NumberFormatter()
        formatter.locale = .current
        formatter.minimumFractionDigits = value.rounded() == value ? 0 : 1
        formatter.maximumFractionDigits = 2
        return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
    }
}

private struct VariantPreviewView: View {
    let variant: GrowthLapseClipVariant
    @State private var player = AVPlayer()

    var body: some View {
        VideoPlayer(player: player)
            .onAppear {
                loadVariant()
            }
            .onChange(of: variant.id) { _ in
                loadVariant()
            }
            .onChange(of: variant.sourcePath) { _ in
                loadVariant()
            }
    }

    private func loadVariant() {
        player.replaceCurrentItem(with: AVPlayerItem(url: variant.sourceURL))
        player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
    }
}
