import ForgejoKit
import SwiftData
import SwiftUI

// swiftlint:disable:next type_body_length
struct InstanceFormView: View {
    enum Mode: Identifiable {
        case add
        case edit(ForgejoInstance)

        var id: String {
            switch self {
            case .add: "add"
            case let .edit(instance): instance.serverURL + instance.username
            }
        }
    }

    enum AuthMode: String, CaseIterable {
        case credentials = "Password"
        case token = "API Token"
    }

    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss
    @Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance]
    @State private var authService: AuthenticationService

    let mode: Mode

    #if DEBUG
        @AppStorage("dev_serverURL") private var devServerURL = ""
        @AppStorage("dev_username") private var devUsername = ""
        @AppStorage("dev_password") private var devPassword = ""
        @AppStorage("dev_apiToken") private var devApiToken = ""
        @AppStorage("dev_allowSelfSigned") private var devAllowSelfSigned = false
    #endif

    @State private var authMode: AuthMode = .credentials
    @State private var name = ""
    @State private var serverURL = ""
    @State private var username = ""
    @State private var password = ""
    @State private var apiToken = ""
    @State private var allowSelfSignedCertificates = false
    @State private var isDefault = false
    @State private var otpCode = ""
    @State private var needsOTP = false
    @State private var isLoading = false
    @State private var errorMessage: String?
    @State private var showError = false

    init(authService: AuthenticationService, mode: Mode) {
        self.authService = authService
        self.mode = mode
    }

    var body: some View {
        NavigationStack {
            Form {
                Section("Forgejo Instance") {
                    TextField("Name (optional)", text: $name)
                        .autocorrectionDisabled()
                        .accessibilityIdentifier("instance-name-field")

                    TextField("Server URL", text: $serverURL)
                        .textContentType(.URL)
                        .autocapitalization(.none)
                        .autocorrectionDisabled()
                        .keyboardType(.URL)
                        .accessibilityIdentifier("login-serverURL-field")
                }

                Section {
                    if authMode == .credentials {
                        TextField("Username", text: $username)
                            .textContentType(.username)
                            .autocapitalization(.none)
                            .autocorrectionDisabled()
                            .accessibilityIdentifier("login-username-field")

                        SecureField("Password", text: $password)
                            .textContentType(.password)
                            .accessibilityIdentifier("login-password-field")

                        if needsOTP {
                            TextField("One-Time Code", text: $otpCode)
                                .keyboardType(.numberPad)
                                .textContentType(.oneTimeCode)
                                .accessibilityIdentifier("login-otp-field")
                        }
                    } else {
                        SecureField("API Token", text: $apiToken)
                            .textContentType(.password)
                            .autocorrectionDisabled()
                            .accessibilityIdentifier("login-token-field")
                    }
                } header: {
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Credentials")
                        Picker("Login Method", selection: $authMode) {
                            ForEach(AuthMode.allCases, id: \.self) { mode in
                                Text(mode.rawValue).tag(mode)
                            }
                        }
                        .pickerStyle(.segmented)
                    }
                    .textCase(nil)
                    .padding(.bottom, 4)
                } footer: {
                    if authMode == .token, case .edit = mode {
                        Text("Leave token blank to keep the existing one.")
                    } else if authMode == .token {
                        Text(
                            """
                            Generate a token under Settings → Applications with the following scopes:
                            • read/write:user
                            • read/write:repository
                            • read/write:issue
                            • read/write:notification
                            • read:organization
                            """,
                        )
                    } else if needsOTP {
                        Text("Enter the 6-digit code from your authenticator app.")
                    } else if case .edit = mode {
                        Text("Leave password blank to keep the existing one.")
                    }
                }

                Section {
                    Toggle("Accept Self-Signed Certificates", isOn: $allowSelfSignedCertificates)
                } footer: {
                    Text("Enable this if your Forgejo instance uses a self-signed SSL certificate.")
                        .font(.caption)
                }

                Section {
                    Toggle("Default Instance", isOn: $isDefault)
                } footer: {
                    Text("The default instance will be connected automatically on launch.")
                        .font(.caption)
                }

                Section {
                    Button(action: handleSave) {
                        HStack {
                            Spacer()
                            if isLoading {
                                ProgressView()
                                    .progressViewStyle(.circular)
                            } else {
                                Text(isAddMode ? "Login" : "Save")
                                    .fontWeight(.semibold)
                            }
                            Spacer()
                        }
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(
                        isLoading || serverURL.isEmpty
                            || (authMode == .credentials
                                && (username.isEmpty || (isAddMode && password.isEmpty)
                                    || (needsOTP && otpCode.isEmpty)))
                            || (authMode == .token && isAddMode && apiToken.isEmpty),
                    )
                    .listRowBackground(Color.clear)
                    .accessibilityIdentifier("login-button")
                }
            }
            .navigationTitle(isAddMode ? "Add Instance" : "Edit Instance")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }
            }
            .errorAlert("Login Failed", message: $errorMessage, isPresented: $showError)
            .onAppear {
                populateFields()
            }
        }
    }

    private var isAddMode: Bool {
        if case .add = mode { return true }
        return false
    }

    private func populateFields() {
        switch mode {
        case .add:
            #if DEBUG
                if !devServerURL.isEmpty { serverURL = devServerURL }
                if !devUsername.isEmpty { username = devUsername }
                if !devPassword.isEmpty { password = devPassword }
                if !devApiToken.isEmpty { apiToken = devApiToken }
                allowSelfSignedCertificates = devAllowSelfSigned
            #endif
        case let .edit(instance):
            name = instance.name
            serverURL = instance.serverURL
            username = instance.username
            allowSelfSignedCertificates = instance.allowSelfSignedCertificates
            isDefault = instance.isDefault
            authMode = instance.useTokenAuth ? .token : .credentials
        }
    }

    private func performLogin() async throws {
        switch authMode {
        case .token:
            try await authService.loginWithToken(
                serverURL: serverURL,
                token: apiToken,
                allowSelfSigned: allowSelfSignedCertificates,
            )
        case .credentials:
            if needsOTP, !otpCode.isEmpty {
                try await authService.loginWithOTP(
                    serverURL: serverURL,
                    username: username,
                    password: password,
                    otp: otpCode,
                    allowSelfSigned: allowSelfSignedCertificates,
                )
            } else {
                try await authService.login(
                    serverURL: serverURL,
                    username: username,
                    password: password,
                    allowSelfSigned: allowSelfSignedCertificates,
                )
            }
        }
    }

    private var editingInstanceId: String? {
        if case let .edit(instance) = mode { return instance.serverURL + instance.username }
        return nil
    }

    // swiftlint:disable:next function_body_length cyclomatic_complexity
    private func handleSave() {
        if !name.isEmpty {
            let conflict = instances.first { inst in
                let instId = inst.serverURL + inst.username
                return inst.name == name && instId != editingInstanceId
            }
            if conflict != nil {
                errorMessage = "An instance named \"\(name)\" already exists. Please choose a different name."
                showError = true
                return
            }
        }

        isLoading = true
        errorMessage = nil

        Task {
            do {
                switch mode {
                case .add:
                    try await performLogin()

                    // Resolve the account identity after login: token auth learns the
                    // real username from the server, so it isn't known before this point.
                    let normalizedURL = ForgejoClient.normalizeServerURL(serverURL)
                    let resolvedUsername =
                        authMode == .token ? (authService.currentUser?.login ?? username) : username

                    // Two accounts sharing a server+username collide on the same sourceKey,
                    // which used to crash merged overviews. Reject the duplicate up front.
                    if rejectDuplicateAccount(serverURL: normalizedURL, username: resolvedUsername) {
                        return
                    }

                    if isDefault {
                        for inst in instances where inst.isDefault {
                            inst.isDefault = false
                        }
                    }

                    let instance = ForgejoInstance(
                        serverURL: normalizedURL,
                        username: resolvedUsername,
                        name: name,
                        isDefault: isDefault,
                        allowSelfSignedCertificates: allowSelfSignedCertificates,
                        useTokenAuth: authMode == .token,
                    )
                    modelContext.insert(instance)
                    do {
                        try modelContext.save()
                    } catch {
                        assertionFailure("SwiftData save failed: \(error)")
                    }
                    authService.currentInstance = instance

                case let .edit(instance):
                    // Resolve the would-be identity before mutating the instance: token
                    // auth may change the username, and credentials auth may too.
                    var resolvedUsername = instance.username
                    if authMode == .token, !apiToken.isEmpty {
                        try await performLogin()
                        resolvedUsername = authService.currentUser?.login ?? username
                    } else if authMode == .credentials, !password.isEmpty {
                        try await performLogin()
                        resolvedUsername = username
                    }

                    let normalizedURL = ForgejoClient.normalizeServerURL(serverURL)

                    // Editing must not produce a server+username that already belongs to
                    // another account, since that collides on the same sourceKey.
                    if rejectDuplicateAccount(
                        serverURL: normalizedURL, username: resolvedUsername, excluding: instance,
                    ) {
                        return
                    }

                    instance.username = resolvedUsername
                    instance.name = name
                    instance.serverURL = normalizedURL
                    instance.allowSelfSignedCertificates = allowSelfSignedCertificates
                    instance.useTokenAuth = authMode == .token

                    if isDefault, !instance.isDefault {
                        for inst in instances where inst.isDefault {
                            inst.isDefault = false
                        }
                    }
                    instance.isDefault = isDefault

                    do {
                        try modelContext.save()
                    } catch {
                        assertionFailure("SwiftData save failed: \(error)")
                    }
                    authService.currentInstance = instance
                }

                dismiss()
            } catch AuthenticationError.otpRequired {
                needsOTP = true
            } catch {
                errorMessage = error.localizedDescription
                showError = true
            }

            isLoading = false
        }
    }

    /// Sets the duplicate-account error and returns true if another stored account
    /// already uses this server+username. `excluding` skips the instance being edited.
    private func rejectDuplicateAccount(
        serverURL normalizedURL: String,
        username: String,
        excluding: ForgejoInstance? = nil,
    ) -> Bool {
        guard Self.accountAlreadyExists(
            serverURL: normalizedURL, username: username, in: instances, excluding: excluding,
        ) else { return false }
        errorMessage = "An account for \"\(username)\" on \(normalizedURL) already exists."
        showError = true
        isLoading = false
        return true
    }

    /// True if `instances` already contains an account with this server+username,
    /// ignoring `excluding` (the instance being edited). Accounts that share a
    /// server+username collide on the same sourceKey, which crashes merged overviews.
    static func accountAlreadyExists(
        serverURL normalizedURL: String,
        username: String,
        in instances: [ForgejoInstance],
        excluding: ForgejoInstance? = nil,
    ) -> Bool {
        instances.contains { inst in
            inst !== excluding && inst.serverURL == normalizedURL && inst.username == username
        }
    }
}

#if DEBUG
    #Preview("Add") {
        InstanceFormView(authService: .previewDefault, mode: .add)
            .modelContainer(for: ForgejoInstance.self, inMemory: true)
    }

    #Preview("Edit") {
        InstanceFormView(authService: .previewDefault, mode: .edit(.preview))
            .modelContainer(for: ForgejoInstance.self, inMemory: true)
    }
#endif
