//
//  ConfigView.swift
//  SwanDesk — configuration window: profile list + editor + live log.
//

import SwiftUI
import UniformTypeIdentifiers

/// Which tab the configuration window should show. Set by the menu before opening
/// the window so "Configuration…" and "Show Log…" land on the right tab.
enum ConfigTab: Hashable {
    case connections, log
}

@MainActor
@Observable
final class ConfigWindowModel {
    static let shared = ConfigWindowModel()
    var selectedTab: ConfigTab = .connections
    private init() {}
}

struct ConfigView: View {
    @Bindable var store: ProfileStore
    @Bindable var connection: ConnectionManager
    @Bindable var windowModel = ConfigWindowModel.shared
    @State private var selection: UUID?
    @State private var profilePendingDeletion: VPNProfile?
    /// A read, validated `.swandesk` document waiting for its passphrase.
    @State private var pendingImport: ProfileExportDocument?
    @State private var importError: String?

    var body: some View {
        TabView(selection: $windowModel.selectedTab) {
            connectionsTab
                .tabItem { Label("Connections", systemImage: "network") }
                .tag(ConfigTab.connections)
            LogPane(connection: connection)
                .tabItem { Label("Log", systemImage: "text.alignleft") }
                .tag(ConfigTab.log)
        }
        .padding()
    }

    private var connectionsTab: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach(store.profiles) { profile in
                    Text(profile.name)
                        .tag(profile.id)
                        // Swipe left on a row to reveal Delete.
                        .swipeActions(edge: .trailing) {
                            Button(role: .destructive) {
                                profilePendingDeletion = profile
                            } label: {
                                Label("Delete", systemImage: "trash")
                            }
                        }
                }
            }
            .frame(minWidth: 180)
            // Always-visible add/remove bar at the bottom of the source list (the
            // standard macOS pattern), so the controls never hide in the window
            // toolbar's overflow.
            .safeAreaInset(edge: .bottom) {
                HStack(spacing: 2) {
                    Button {
                        let p = store.add(); selection = p.id
                    } label: {
                        Image(systemName: "plus").frame(width: 24, height: 22)
                    }
                    .help("Add a profile")

                    Button {
                        if let id = selection, let p = store.profiles.first(where: { $0.id == id }) {
                            profilePendingDeletion = p
                        }
                    } label: {
                        Image(systemName: "minus").frame(width: 24, height: 22)
                    }
                    .help("Delete the selected profile")
                    .disabled(selection == nil)

                    Button {
                        beginImport()
                    } label: {
                        Image(systemName: "square.and.arrow.down").frame(width: 24, height: 22)
                    }
                    .help("Import a profile from a .swandesk file")

                    Spacer()
                }
                .buttonStyle(.borderless)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(.bar)
            }
        } detail: {
            if let id = selection, store.profiles.contains(where: { $0.id == id }) {
                ProfileEditor(store: store, connection: connection, profileID: id)
                    .id(id)
            } else {
                ContentUnavailableView("No Profile Selected",
                                       systemImage: "lock.shield",
                                       description: Text("Select a profile or add a new one."))
            }
        }
        .onAppear { if selection == nil { selection = store.profiles.first?.id } }
        // Confirm before deleting — deletion also discards the saved password/keys.
        .alert("Delete Profile",
               isPresented: Binding(get: { profilePendingDeletion != nil },
                                    set: { if !$0 { profilePendingDeletion = nil } }),
               presenting: profilePendingDeletion) { profile in
            Button("Delete", role: .destructive) { deleteProfile(profile) }
            Button("Cancel", role: .cancel) { }
        } message: { profile in
            Text("Delete “\(profile.name)”? This also removes its saved password and certificates, and can’t be undone.")
        }
        // Passphrase prompt shown after a valid .swandesk file is picked.
        .sheet(item: $pendingImport) { document in
            ImportPassphraseSheet(profileName: document.name) { passphrase in
                finishImport(document, passphrase: passphrase)
            }
        }
        .alert("Couldn’t Import Profile",
               isPresented: Binding(get: { importError != nil },
                                    set: { if !$0 { importError = nil } }),
               presenting: importError) { _ in
            Button("OK", role: .cancel) { }
        } message: { msg in Text(msg) }
    }

    private func deleteProfile(_ profile: VPNProfile) {
        let wasSelected = selection == profile.id
        store.delete(profile)
        if wasSelected { selection = store.profiles.first?.id }
        profilePendingDeletion = nil
    }

    /// Pick a `.swandesk` file, validate the envelope, then prompt for the
    /// passphrase (in the sheet bound to `pendingImport`).
    private func beginImport() {
        let panel = NSOpenPanel()
        panel.allowedContentTypes = [UTType(filenameExtension: "swandesk") ?? .json, .json]
        panel.allowsMultipleSelection = false
        guard panel.runModal() == .OK, let url = panel.url else { return }
        do {
            let data = try Data(contentsOf: url)
            pendingImport = try ProfilePortability.readDocument(data)
        } catch {
            importError = error.localizedDescription
        }
    }

    /// Decrypt the pending document with `passphrase` and store the new profile.
    private func finishImport(_ document: ProfileExportDocument, passphrase: String) {
        do {
            let (profile, secrets) = try ProfilePortability.decode(document, passphrase: passphrase)
            let result = store.importProfile(profile, secrets: secrets)
            selection = result.profile.id
            if !result.secretsSaved {
                importError = "The profile was imported, but its password and keys couldn’t be saved to your Keychain (it may be locked). Open the profile and Save again."
            }
        } catch {
            importError = error.localizedDescription
        }
    }
}

/// Editor for a single profile. Edits a local copy and writes back on Save.
private struct ProfileEditor: View {
    @Bindable var store: ProfileStore
    @Bindable var connection: ConnectionManager
    let profileID: UUID

    @State private var profile = VPNProfile()
    @State private var secrets = VPNSecrets()
    @State private var routesText = ""
    @State private var dnsText = ""
    @State private var searchText = ""
    @State private var loaded = false
    @State private var saveError: String?
    @State private var showExportSheet = false
    @State private var exportError: String?

    var body: some View {
        Form {
            Section("General") {
                TextField("Name", text: $profile.name)
            }
            Section("Authentication") {
                Picker("Method", selection: $profile.authMethod) {
                    ForEach(VPNAuthMethod.allCases) { method in
                        Text(method.label).tag(method)
                    }
                }
                // Certificate-only and PSK auth run under either IKE version, so
                // offer a version picker; the other methods imply their version
                // (XAuth → 1, EAP → 2) and we keep `ikeVersion` in sync via onChange.
                if profile.authMethod.requiredIKEVersion == nil {
                    Picker("IKE version", selection: $profile.ikeVersion) {
                        Text("IKEv1").tag(1)
                        Text("IKEv2").tag(2)
                    }
                    .pickerStyle(.segmented)
                }

                if profile.authMethod.usesPassword {
                    TextField("User", text: $profile.xauthID)
                    SecureField("Password", text: $secrets.xauthPassword)
                }
                if profile.authMethod.usesPSK {
                    SecureField("Pre-shared key", text: $secrets.xauthPassword)
                }
                if profile.authMethod == .certificate || profile.authMethod == .eapTLS
                    || profile.authMethod.usesPSK {
                    TextField(profile.authMethod.localIDHint, text: $profile.localID)
                }

                if profile.authMethod.verifiesGatewayWithCert {
                    certField("CA certificate", pem: $profile.caCertPEM)
                }
                if profile.authMethod.usesClientCert {
                    certField("Client certificate", pem: $profile.clientCertPEM)
                    certField("Client private key", pem: $secrets.privateKeyPEM)
                }
            }
            .onChange(of: profile.authMethod) { _, method in
                // Snap the IKE version to one the method allows.
                if let required = method.requiredIKEVersion { profile.ikeVersion = required }
            }
            Section("Gateway") {
                TextField("Remote address", text: $profile.remoteAddr)
                TextField("Remote ID (DN, blank = %any)", text: $profile.remoteID)
                TextField("IKE proposals", text: $profile.ikeProposals)
                TextField("ESP proposals", text: $profile.espProposals)
                TextField("Remote traffic selector", text: $profile.remoteTS)
                Toggle("Request virtual IP", isOn: $profile.requestVIP)
            }
            Section("Routing & DNS") {
                LabeledContent("Routes (CIDR, comma/space separated)") {
                    TextField("", text: $routesText, axis: .vertical).lineLimit(1...4)
                }
                LabeledContent("DNS servers") {
                    TextField("", text: $dnsText).textFieldStyle(.roundedBorder)
                }
                LabeledContent("Search domains") {
                    TextField("", text: $searchText).textFieldStyle(.roundedBorder)
                }
            }
            Section("Keepalive") {
                TextField("Ping IP (blank disables)", text: $profile.keepaliveIP)
                TextField("Interval (s)", value: $profile.keepaliveInterval, format: .number)
            }
        }
        .formStyle(.grouped)
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                Button("Save") { save() }
            }
            ToolbarItem {
                Button("Export…") { save(); showExportSheet = true }
                    .help("Export this profile to a passphrase-protected .swandesk file")
            }
            ToolbarItem {
                Button(isActive ? "Disconnect" : "Connect") {
                    save()
                    if isActive { connection.disconnect() }
                    else { connection.connect(profile: profile, secrets: secrets) }
                }
                .disabled(connection.busy)
            }
        }
        .onAppear(perform: loadIfNeeded)
        .sheet(isPresented: $showExportSheet) {
            ExportPassphraseSheet { passphrase in exportProfile(passphrase: passphrase) }
        }
        .alert("Couldn’t Save Secrets",
               isPresented: Binding(get: { saveError != nil },
                                    set: { if !$0 { saveError = nil } }),
               presenting: saveError) { _ in
            Button("OK", role: .cancel) { }
        } message: { msg in Text(msg) }
        .alert("Couldn’t Export Profile",
               isPresented: Binding(get: { exportError != nil },
                                    set: { if !$0 { exportError = nil } }),
               presenting: exportError) { _ in
            Button("OK", role: .cancel) { }
        } message: { msg in Text(msg) }
    }

    private var isActive: Bool {
        connection.status.profileID == profileID.uuidString && connection.isConnected
    }

    private func loadIfNeeded() {
        guard !loaded else { return }
        if let p = store.profiles.first(where: { $0.id == profileID }) { profile = p }
        secrets = store.secrets(for: profileID)
        routesText = profile.routes.joined(separator: ", ")
        dnsText = profile.dnsServers.joined(separator: ", ")
        searchText = profile.searchDomains.joined(separator: " ")
        loaded = true
    }

    private func save() {
        guard loaded else { return }   // never persist the unloaded blank seed
        profile.routes = splitList(routesText)
        profile.dnsServers = splitList(dnsText)
        profile.searchDomains = splitList(searchText)
        store.update(profile)
        if !store.setSecrets(secrets, for: profileID) {
            saveError = "The password and keys couldn’t be saved to your Keychain (it may be locked). Unlock your Keychain and Save again."
        }
    }

    private func splitList(_ s: String) -> [String] {
        s.split(whereSeparator: { $0 == "," || $0 == " " || $0 == "\n" })
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
    }

    /// Encrypt the current profile's secrets under `passphrase` and write a
    /// `.swandesk` file to a user-chosen location.
    private func exportProfile(passphrase: String) {
        do {
            let data = try ProfilePortability.makeExportData(profile: profile,
                                                             secrets: secrets,
                                                             passphrase: passphrase)
            let panel = NSSavePanel()
            panel.nameFieldStringValue = "\(sanitizedFileName(profile.name)).swandesk"
            panel.allowedContentTypes = [UTType(filenameExtension: "swandesk") ?? .json]
            panel.canCreateDirectories = true
            guard panel.runModal() == .OK, var url = panel.url else { return }
            if url.pathExtension != "swandesk" { url.appendPathExtension("swandesk") }
            try data.write(to: url, options: .atomic)
        } catch {
            exportError = error.localizedDescription
        }
    }

    private func sanitizedFileName(_ name: String) -> String {
        let trimmed = name.trimmingCharacters(in: .whitespaces)
        let cleaned = trimmed.replacingOccurrences(of: "/", with: "-")
            .replacingOccurrences(of: ":", with: "-")
        return cleaned.isEmpty ? "profile" : cleaned
    }

    @ViewBuilder
    private func certField(_ title: String, pem: Binding<String>) -> some View {
        HStack {
            Text(title)
            Spacer()
            Text(pem.wrappedValue.isEmpty ? "Not set" : "Loaded")
                .foregroundStyle(pem.wrappedValue.isEmpty ? Color.secondary : Color.green)
            Button("Import…") { importPEM(into: pem) }
        }
    }

    private func importPEM(into pem: Binding<String>) {
        let panel = NSOpenPanel()
        panel.allowedContentTypes = [UTType(filenameExtension: "pem") ?? .data,
                                     UTType(filenameExtension: "crt") ?? .data,
                                     UTType(filenameExtension: "key") ?? .data,
                                     .x509Certificate, .data]
        panel.allowsMultipleSelection = false
        if panel.runModal() == .OK, let url = panel.url,
           let contents = try? String(contentsOf: url, encoding: .utf8) {
            pem.wrappedValue = contents
        }
    }
}

/// Asks for a passphrase (entered twice) to encrypt an exported profile.
private struct ExportPassphraseSheet: View {
    let onExport: (String) -> Void
    @Environment(\.dismiss) private var dismiss
    @State private var passphrase = ""
    @State private var confirm = ""

    private var canExport: Bool { !passphrase.isEmpty && passphrase == confirm }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Export Profile").font(.headline)
            Text("Choose a passphrase to protect the exported file. You’ll need it to import the profile on another machine.")
                .font(.callout).foregroundStyle(.secondary)
                .fixedSize(horizontal: false, vertical: true)
            SecureField("Passphrase", text: $passphrase)
            SecureField("Confirm passphrase", text: $confirm)
            if !confirm.isEmpty && passphrase != confirm {
                Text("Passphrases don’t match.").font(.caption).foregroundStyle(.red)
            }
            HStack {
                Spacer()
                Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction)
                Button("Export") { onExport(passphrase); dismiss() }
                    .keyboardShortcut(.defaultAction)
                    .disabled(!canExport)
            }
        }
        .padding(20)
        .frame(width: 380)
    }
}

/// Asks for the passphrase needed to decrypt an imported profile.
private struct ImportPassphraseSheet: View {
    let profileName: String
    let onImport: (String) -> Void
    @Environment(\.dismiss) private var dismiss
    @State private var passphrase = ""

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Import “\(profileName)”").font(.headline)
            Text("Enter the passphrase this profile was exported with.")
                .font(.callout).foregroundStyle(.secondary)
                .fixedSize(horizontal: false, vertical: true)
            SecureField("Passphrase", text: $passphrase)
            HStack {
                Spacer()
                Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction)
                Button("Import") { onImport(passphrase); dismiss() }
                    .keyboardShortcut(.defaultAction)
                    .disabled(passphrase.isEmpty)
            }
        }
        .padding(20)
        .frame(width: 380)
    }
}

/// Live log pane backed by the daemon's pushed log lines.
private struct LogPane: View {
    @Bindable var connection: ConnectionManager

    var body: some View {
        VStack(alignment: .leading) {
            ScrollView {
                ScrollViewReader { _ in
                    Text(connection.logLines.joined(separator: "\n"))
                        .font(.system(.caption, design: .monospaced))
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
            }
            HStack {
                Spacer()
                Button("Clear") { connection.clearLog() }
            }
        }
    }
}
