//
//  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?
    @Environment(\.openURL) private var openURL

    private static let juvexURL = URL(string: "https://www.juvex.fi/")!

    var body: some View {
        VStack(spacing: 8) {
            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)
            }
            brandingFooter
        }
        .padding()
    }

    /// Juvex branding, bottom corner of the settings window.
    private var brandingFooter: some View {
        HStack {
            Spacer()
            Text("Made by Juvex.")
                .font(.caption)
                .foregroundStyle(.secondary)
            Button {
                openURL(Self.juvexURL)
            } label: {
                Image("JuvexLogo")
                    .resizable()
                    .scaledToFit()
                    .frame(height: 26)
            }
            .buttonStyle(.plain)
            .accessibilityLabel("Juvex")
            .accessibilityAddTraits(.isButton)
            .help(Self.juvexURL.absoluteString)
        }
    }

    private var connectionsTab: some View {
        NavigationSplitView {
            VStack(spacing: 0) {
                List(selection: $selection) {
                    // Rows are drag-reorderable: list position = priority among
                    // simultaneously connected tunnels (top = highest — wins route,
                    // DNS and dnsmasq conflicts).
                    ForEach(store.profiles) { profile in
                        HStack {
                            Text(profile.name)
                            Spacer()
                            if let state = connection.tunnelStatus(for: profile.id)?.state,
                               state != .disconnected {
                                Circle()
                                    .fill(state.uiColor)
                                    .frame(width: 8, height: 8)
                            }
                        }
                        .tag(profile.id)
                        // Swipe left on a row to reveal Delete.
                        .swipeActions(edge: .trailing) {
                            Button(role: .destructive) {
                                profilePendingDeletion = profile
                            } label: {
                                Label("Delete", systemImage: "trash")
                            }
                        }
                    }
                    .onMove { source, destination in
                        store.move(fromOffsets: source, toOffset: destination)
                        connection.pushPriorities(store.profiles.map(\.id))
                    }
                }

                // Rendered outside the List (macOS sidebar list rows are
                // single-line and won't wrap), so it can span multiple lines.
                if store.profiles.count > 1 {
                    Text("Drag to reorder — profiles higher in the list take priority when connected tunnels have overlapping routes or DNS.")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                        .multilineTextAlignment(.leading)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                }
            }
            .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)
        // Deleting shifts the remaining profiles' list positions — sync the
        // helper so active tunnels' priorities keep matching the visible order.
        connection.pushPriorities(store.profiles.map(\.id))
        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?
    @State private var certImportError: String?

    var body: some View {
        formBody
            .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(profileID: profileID)
                        } else {
                            initiateConnect()
                        }
                    }
                    .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) }
            .alert("Couldn’t Import File",
                   isPresented: Binding(get: { certImportError != nil },
                                        set: { if !$0 { certImportError = nil } }),
                   presenting: certImportError) { _ in
                Button("OK", role: .cancel) { }
            } message: { msg in Text(msg) }
    }

    @ViewBuilder
    private var formBody: some View {
        Form {
            Section("General") {
                LabeledContent("Name") {
                    TextField("", text: $profile.name).textFieldStyle(.roundedBorder)
                }
                Toggle("Connect automatically when SwanDesk launches", isOn: $profile.connectOnLaunch)
            }
            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 {
                    LabeledContent("User") {
                        TextField("", text: $profile.xauthID).textFieldStyle(.roundedBorder)
                    }
                    LabeledContent("Password") {
                        SecureField("", text: $secrets.xauthPassword).textFieldStyle(.roundedBorder)
                    }
                }
                if profile.authMethod.usesPSK {
                    LabeledContent("Pre-shared key") {
                        SecureField("", text: $secrets.xauthPassword).textFieldStyle(.roundedBorder)
                    }
                }
                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") {
                LabeledContent("Remote address") {
                    TextField("", text: $profile.remoteAddr).textFieldStyle(.roundedBorder)
                }
                LabeledContent("Remote ID (DN, blank = %any)") {
                    TextField("", text: $profile.remoteID).textFieldStyle(.roundedBorder)
                }
                LabeledContent("Remote traffic selector") {
                    TextField("", text: $profile.remoteTS).textFieldStyle(.roundedBorder)
                }
                Toggle("Request virtual IP", isOn: $profile.requestVIP)
            }
            Section("IKE Proposals") {
                ProposalPicker(proposal: $profile.ikeProposals)
            }
            Section("ESP Proposals") {
                ProposalPicker(proposal: $profile.espProposals, showNoPFS: true)
            }
            Section("Routing & DNS") {
                VStack(alignment: .leading, spacing: 4) {
                    Text(routesText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                         ? "Routes (one CIDR per line — leave empty to use the negotiated traffic selector)"
                         : "Routes (one CIDR per line)")
                        .foregroundStyle(.secondary)
                        .font(.caption)
                    TextEditor(text: $routesText)
                        .font(.system(.body, design: .monospaced))
                        .multilineTextAlignment(.leading)
                        .frame(minHeight: 60)
                }
                VStack(alignment: .leading, spacing: 4) {
                    Text("DNS servers")
                        .foregroundStyle(.secondary)
                        .font(.caption)
                    TextEditor(text: $dnsText)
                        .font(.system(.body, design: .monospaced))
                        .multilineTextAlignment(.leading)
                        .frame(minHeight: 60)
                }
                LabeledContent("Search domains") {
                    TextField("", text: $searchText).textFieldStyle(.roundedBorder)
                }
                Toggle("Disable IPv6 on all interfaces while connected (prevents IPv6 leaks when the VPN carries only IPv4)", isOn: $profile.preventIPv6Leak)
                VStack(alignment: .leading, spacing: 2) {
                    Toggle("Kill switch", isOn: $profile.killSwitchEnabled)
                    Text("If the VPN unexpectedly disconnects or stops working, all network traffic is blocked to prevent any data from leaking outside the VPN connection. Traffic is unblocked when you disconnect manually.")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                Toggle("Use dnsmasq (local DNS proxy on 127.0.0.1:53)", isOn: $profile.dnsmasqEnabled)
                if profile.dnsmasqEnabled {
                    let isCustomConfig = !profile.dnsmasqConfig.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                    VStack(alignment: .leading, spacing: 4) {
                        Text(isCustomConfig
                             ? "dnsmasq config (used as complete config file — include listen-address, port, server= etc.)"
                             : "dnsmasq config (leave empty to auto-generate from DNS servers above)")
                            .foregroundStyle(.secondary)
                            .font(.caption)
                        TextEditor(text: $profile.dnsmasqConfig)
                            .font(.system(.body, design: .monospaced))
                            .multilineTextAlignment(.leading)
                            .frame(minHeight: 80)
                        if isCustomConfig {
                            Text("Tip: add resolv-file=\(StagingPaths.upstreamResolvConf) to forward queries not otherwise matched to the DNS servers this Mac used just before connecting. The file is written once the tunnel connects.")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                                .fixedSize(horizontal: false, vertical: true)
                        } else if dnsText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                            Text("No DNS servers configured above — dnsmasq will forward to the DNS servers this Mac used just before connecting.")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                                .fixedSize(horizontal: false, vertical: true)
                        }
                    }
                }
            }
            Section("Keepalive") {
                LabeledContent("Ping IP (blank disables)") {
                    TextField("", text: $profile.keepaliveIP).textFieldStyle(.roundedBorder)
                }
                LabeledContent("Interval (s)") {
                    TextField("", value: $profile.keepaliveInterval, format: .number).textFieldStyle(.roundedBorder)
                }
            }
        }
    }

    private var isActive: Bool {
        connection.isActive(profileID)
    }

    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: "\n")
        dnsText = profile.dnsServers.joined(separator: "\n")
        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 initiateConnect() {
        // save() has already persisted the edited secrets to the store, so the
        // shared resolve (store lookup + prompt when empty) sees current values.
        guard let s = connection.resolveSecrets(for: profile) else { return }
        connection.connect(profile: profile, secrets: s)
    }

    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
        guard panel.runModal() == .OK, let url = panel.url else { return }
        do {
            pem.wrappedValue = try String(contentsOf: url, encoding: .utf8)
        } catch {
            certImportError = error.localizedDescription
        }
    }
}

/// 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 { proxy in
                    Text(connection.logLines.joined(separator: "\n"))
                        .font(.system(.caption, design: .monospaced))
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .id("bottom")
                        .onChange(of: connection.logLines.count) {
                            proxy.scrollTo("bottom", anchor: .bottom)
                        }
                }
            }
            HStack {
                Spacer()
                Button("Clear") { connection.clearLog() }
            }
        }
    }
}
