import IndieSync
import SwiftUI

/// Composer for a new note and editor for an existing one. On create it
/// snapshots the current context (refreshing location in the background);
/// on edit the original capture context is preserved untouched.
///
/// Create mode is text-first: the text editor is focused immediately, with
/// an "add media" bar below it for attaching other content. Today that bar
/// has one medium — voice — which reveals the recording flow inline. Edit
/// mode plays back any attached audio, shows transcription status, and
/// offers a per-note "Re-transcribe" override.
struct NoteEditorView: View {
    enum Mode {
        case create
        case edit(NoteEntity)
    }

    /// A kind of content that can be attached to a note from the "add media"
    /// bar in create mode. Only `.audio` exists today; adding a future medium
    /// (photo, file, link) means adding a case here plus its icon/label.
    private enum CaptureMedium: CaseIterable, Hashable {
        case audio

        var icon: String {
            switch self {
            case .audio: "mic"
            }
        }

        var label: String {
            switch self {
            case .audio: "Add Voice"
            }
        }
    }

    let mode: Mode

    /// Project a note created from this editor is pre-filed under (e.g. when
    /// composing from inside `ProjectNotesView`). Ignored on the edit path —
    /// `updateNote` only rewrites text/tags on the loaded note, so an
    /// existing note's `payload.project` is left untouched.
    var defaultProject: String? = nil

    @Environment(SyncEngine.self) private var syncEngine
    @Environment(ContextService.self) private var contextService
    @Environment(TranscriptionService.self) private var transcriptionService
    @Environment(TranscriptionSettings.self) private var transcriptionSettings
    @Environment(\.dismiss) private var dismiss

    @State private var text = ""
    /// The note text as last loaded from the entity, so an external change
    /// (a transcription landing while this editor is open) can be told apart
    /// from the user's own typing: refresh only while `text == loadedText`.
    @State private var loadedText = ""
    @State private var tagsText = ""
    @State private var saveError: String?
    @State private var recordedAudio: AudioRecorderService.Recording?
    /// The medium currently expanded in the "add media" bar (revealing its
    /// capture flow), or `nil` when the bar is showing only its buttons.
    @State private var expandedMedium: CaptureMedium?
    @FocusState private var textFocused: Bool

    var body: some View {
        NavigationStack {
            Form {
                Section {
                    TextEditor(text: $text)
                        .focused($textFocused)
                        .frame(minHeight: 160)
                }

                if isCreating {
                    mediaSection
                }

                if case .edit(let note) = mode, note.hasAudio {
                    audioPlaybackSection(for: note)
                }

                Section("Tags") {
                    TextField("coffee, people, ideas…", text: $tagsText)
                        .autocorrectionDisabled()
                        #if os(iOS)
                        .textInputAutocapitalization(.never)
                        #endif
                }

                Section("Context") {
                    if isCreating {
                        ContextSummaryRow(
                            context: contextService.current,
                            isRefreshing: contextService.isRefreshing
                        )
                    } else if case .edit(let note) = mode {
                        ContextSummaryRow(context: note.context, isRefreshing: false)
                    }
                }

                if let saveError {
                    Section {
                        Label(saveError, systemImage: "exclamationmark.icloud")
                            .foregroundStyle(.red)
                            .font(.footnote)
                    }
                }
            }
            .formStyle(.grouped)
            .navigationTitle(isCreating ? "New Note" : "Edit Note")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") { save() }
                        .disabled(!canSave)
                }
            }
        }
        .task {
            switch mode {
            case .create:
                textFocused = true
                await contextService.refresh()
            case .edit(let note):
                text = note.text
                loadedText = note.text
                tagsText = note.tags.joined(separator: ", ")
            }
        }
        // A transcription finishing while this editor is open updates the
        // entity underneath us; mirror it into the text field as long as the
        // user hasn't typed (their edits always win over a live refresh).
        .onChange(of: editedNote?.text) { _, newValue in
            guard let newValue, text == loadedText else { return }
            text = newValue
            loadedText = newValue
        }
        #if os(macOS)
        .frame(minWidth: 480, minHeight: 420)
        #endif
    }

    // MARK: - Media (create)

    /// The "add media" bar plus whichever capture flow is currently expanded
    /// beneath it. Buttons drop out of the bar once their medium is expanded
    /// or attached, so there's never a stale "Add Voice" button sitting above
    /// an in-progress recording or a finished one.
    @ViewBuilder
    private var mediaSection: some View {
        Section {
            if !availableMedia.isEmpty {
                mediaBar
            }

            if let recordedAudio {
                AudioPlayerView(source: .localURL(recordedAudio.url))
                Button(role: .destructive) {
                    try? FileManager.default.removeItem(at: recordedAudio.url)
                    self.recordedAudio = nil
                } label: {
                    Label("Discard Recording", systemImage: "trash")
                }
            } else if expandedMedium == .audio {
                AudioRecordingControl { url, duration in
                    recordedAudio = AudioRecorderService.Recording(url: url, duration: duration)
                    expandedMedium = nil
                }
                .padding(.vertical, 4)
            }
        }
    }

    private var mediaBar: some View {
        HStack(spacing: 12) {
            ForEach(availableMedia, id: \.self) { medium in
                Button {
                    expandedMedium = medium
                } label: {
                    Label(medium.label, systemImage: medium.icon)
                }
                .buttonStyle(.bordered)
            }
            Spacer()
        }
    }

    /// Media not yet attached and not already expanded below the bar.
    private var availableMedia: [CaptureMedium] {
        CaptureMedium.allCases.filter { $0 != expandedMedium && !isAttached($0) }
    }

    private func isAttached(_ medium: CaptureMedium) -> Bool {
        switch medium {
        case .audio: recordedAudio != nil
        }
    }

    // MARK: - Audio (edit)

    @ViewBuilder
    private func audioPlaybackSection(for note: NoteEntity) -> some View {
        Section("Voice") {
            if let id = ULID(string: note.id) {
                AudioPlayerView(source: .note(id: id, duration: note.audioDuration ?? 0))
            }
            transcriptionStatusRow(for: note)
            retranscribeMenu(for: note)
        }
        // The transcript is machine output, kept apart from the user's own
        // text: shown read-only here (it updates live as attempts land) with
        // a one-tap copy into the editable note body.
        if let transcript = note.transcriptText, !transcript.isEmpty {
            Section("Transcript") {
                Text(transcript)
                    .textSelection(.enabled)
                    .foregroundStyle(.secondary)
                Button {
                    text = text.isEmpty ? transcript : text + "\n\n" + transcript
                } label: {
                    Label("Insert into Note", systemImage: "text.insert")
                }
            }
        }
    }

    @ViewBuilder
    private func transcriptionStatusRow(for note: NoteEntity) -> some View {
        switch note.audio?.transcriptionStatus {
        case .pending:
            if note.transcriberDeviceID == TranscriptionSettings.installID {
                Label {
                    Text("Transcribing…")
                } icon: {
                    ProgressView().controlSize(.small)
                }
                .foregroundStyle(.secondary)
            } else {
                Label("Waiting to transcribe on another device", systemImage: "hourglass")
                    .foregroundStyle(.secondary)
            }
        case .failed:
            HStack {
                Label("Transcription failed", systemImage: "exclamationmark.triangle")
                    .foregroundStyle(.secondary)
                Spacer()
                Button("Retry") { retranscribe(note: note, localeID: nil) }
                    .buttonStyle(.bordered)
                    .controlSize(.small)
            }
        case .done:
            if let localeID = note.audio?.transcriptionLocaleID {
                Label(
                    "Transcribed · \(TranscriptionSettings.displayName(for: Locale(identifier: localeID)))",
                    systemImage: "checkmark.circle"
                )
                .foregroundStyle(.secondary)
                .font(.footnote)
            }
        case .none:
            EmptyView()
        }
    }

    @ViewBuilder
    private func retranscribeMenu(for note: NoteEntity) -> some View {
        let locales = transcriptionSettings.enabledLocales
        if !locales.isEmpty {
            Menu {
                Button("Auto-detect Language") { retranscribe(note: note, localeID: nil) }
                Divider()
                ForEach(locales, id: \.identifier) { locale in
                    Button(TranscriptionSettings.displayName(for: locale)) {
                        retranscribe(note: note, localeID: TranscriptionSettings.canonicalID(locale))
                    }
                }
            } label: {
                Label("Re-transcribe", systemImage: "arrow.clockwise")
            }
        }
    }

    private func retranscribe(note: NoteEntity, localeID: String?) {
        guard let id = ULID(string: note.id) else { return }
        Task {
            try? await syncEngine.requestRetranscription(id: id, localeID: localeID)
            transcriptionService.enqueue(noteID: id, forcedLocaleID: localeID)
        }
    }

    // MARK: - State

    private var isCreating: Bool {
        if case .create = mode { return true }
        return false
    }

    private var editedNote: NoteEntity? {
        if case .edit(let note) = mode { return note }
        return nil
    }

    private var canSave: Bool {
        if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true }
        if recordedAudio != nil { return true }
        if case .edit(let note) = mode, note.hasAudio { return true }
        return false
    }

    private var parsedTags: [String] {
        tagsText
            .split(separator: ",")
            .map { $0.trimmingCharacters(in: .whitespaces).lowercased() }
            .filter { !$0.isEmpty }
    }

    // MARK: - Save

    private func save() {
        guard canSave else { return }
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        Task {
            do {
                switch mode {
                case .create:
                    if let recordedAudio {
                        let note = try await syncEngine.createAudioNote(
                            audioFileURL: recordedAudio.url,
                            duration: recordedAudio.duration,
                            text: trimmed,
                            tags: parsedTags,
                            context: contextService.current,
                            transcriberDeviceID: TranscriptionSettings.installID,
                            project: defaultProject
                        )
                        // Fast path: hand the recording's temp file straight to the
                        // transcriber so it doesn't round-trip through the container.
                        transcriptionService.enqueue(noteID: note.id, localAudioURL: recordedAudio.url)
                    } else {
                        try await syncEngine.createNote(
                            text: trimmed,
                            tags: parsedTags,
                            context: contextService.current,
                            project: defaultProject
                        )
                    }
                case .edit(let entity):
                    guard let id = ULID(string: entity.id),
                          var note = await syncEngine.loadNote(id: id) else { return }
                    note.payload.text = trimmed
                    note.payload.tags = parsedTags
                    try await syncEngine.updateNote(note)
                }
                dismiss()
            } catch {
                saveError = "Couldn't save: \(error.localizedDescription)"
            }
        }
    }
}

/// One-line "where and when" readout shown in the composer, so the user sees
/// what context is being stamped onto the note.
struct ContextSummaryRow: View {
    let context: CaptureContext
    let isRefreshing: Bool

    var body: some View {
        HStack {
            Image(systemName: context.hasCoordinate ? "location.fill" : "location.slash")
                .foregroundStyle(context.hasCoordinate ? Color.accentColor : .secondary)
            if let summary = context.summary {
                Text(summary)
            } else if isRefreshing {
                Text("Finding where you are…")
                    .foregroundStyle(.secondary)
            } else if context.hasCoordinate {
                Text("Location captured")
            } else {
                Text("No location")
                    .foregroundStyle(.secondary)
            }
            Spacer()
            if isRefreshing {
                ProgressView()
                    .controlSize(.small)
            }
        }
        .font(.callout)
    }
}
