import ForgejoKit
import Foundation
import SwiftData

@Observable
class AuthenticationService {
    var isAuthenticated = false
    var currentUser: User?
    var currentInstance: ForgejoInstance?
    private(set) var client: ForgejoClient?

    func login(serverURL: String, username: String, password: String, allowSelfSigned: Bool = false) async throws {
        let result = try await ForgejoClient.login(
            serverURL: serverURL,
            username: username,
            password: password,
            allowSelfSignedCertificates: allowSelfSigned,
        )
        try await storeCredentials(result: result, password: password)
    }

    func loginWithToken(serverURL: String, token: String, allowSelfSigned: Bool = false) async throws {
        let normalizedURL = ForgejoClient.normalizeServerURL(serverURL)
        let tokenClient = ForgejoClient(
            serverURL: normalizedURL,
            username: "",
            token: token,
            allowSelfSignedCertificates: allowSelfSigned,
        )
        let user = try await tokenClient.fetchCurrentUser()
        let tokenClientWithUsername = ForgejoClient(
            serverURL: normalizedURL,
            username: user.login,
            token: token,
            allowSelfSignedCertificates: allowSelfSigned,
        )
        try await KeychainManager.shared.saveToken(
            token, for: normalizedURL, username: user.login,
        )
        client = tokenClientWithUsername
        currentUser = user
        isAuthenticated = true
    }

    func loginWithOTP(
        serverURL: String, username: String, password: String, otp: String,
        allowSelfSigned: Bool = false,
    ) async throws {
        let result = try await ForgejoClient.loginWithOTP(
            serverURL: serverURL,
            username: username,
            password: password,
            otp: otp,
            allowSelfSignedCertificates: allowSelfSigned,
        )
        try await storeCredentials(result: result, password: password)
    }

    private func storeCredentials(result: LoginResult, password: String) async throws {
        try await KeychainManager.shared.savePassword(
            password, for: result.client.serverURL, username: result.client.username,
        )
        try await KeychainManager.shared.saveToken(
            result.token, for: result.client.serverURL, username: result.client.username,
        )
        client = result.client
        currentUser = result.user
        isAuthenticated = true
    }

    func disconnect() {
        isAuthenticated = false
        currentUser = nil
        currentInstance = nil
        client = nil
    }

    /// Synchronous session restore from keychain so the home screen
    /// with cached data can appear on the very first frame.
    @discardableResult
    func bootstrapSession(instance: ForgejoInstance) -> Bool {
        let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL)
        guard let token = KeychainManager.getTokenSync(
            for: normalizedURL, username: instance.username,
        ) else { return false }

        client = ForgejoClient(
            serverURL: normalizedURL,
            username: instance.username,
            token: token,
            allowSelfSignedCertificates: instance.allowSelfSignedCertificates,
        )
        currentInstance = instance
        isAuthenticated = true
        return true
    }

    func logout(modelContext: ModelContext) async {
        if let instance = currentInstance {
            let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL)
            // Remove the instance from SwiftData first. If the save fails, keep the
            // credentials so the account stays usable instead of becoming an
            // unauthenticatable orphan, and bail out without clearing the session.
            instance.isDefault = false
            modelContext.delete(instance)
            do {
                try modelContext.save()
            } catch {
                assertionFailure("SwiftData save failed during logout: \(error)")
                return
            }
            // Delete credentials only after the instance is gone from SwiftData.
            do {
                try await KeychainManager.shared.deleteCredentials(
                    for: normalizedURL,
                    username: instance.username,
                )
            } catch {
                // Keychain delete failed, log in debug builds
                #if DEBUG
                    print("Keychain delete failed during logout: \(error)")
                #endif
            }
        }
        // Clears cache for all instances since keys are hashed and cannot be scoped per-instance
        DiskCache.removeAll()
        isAuthenticated = false
        currentUser = nil
        currentInstance = nil
        client = nil
    }

    func restoreSession(instance: ForgejoInstance) async throws {
        let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL)
        // Migrate keychain items to AfterFirstUnlock accessibility for background access
        await KeychainManager.shared.migrateAccessibility(for: normalizedURL, username: instance.username)
        try await restoreWithCredentials(
            serverURL: normalizedURL,
            username: instance.username,
            allowSelfSigned: instance.allowSelfSignedCertificates,
            useTokenAuth: instance.useTokenAuth,
        )
        currentInstance = instance
    }

    func restoreFromSnapshot(_ snapshot: InstanceSnapshot) async throws {
        let normalizedURL = ForgejoClient.normalizeServerURL(snapshot.serverURL)
        try await restoreWithCredentials(
            serverURL: normalizedURL,
            username: snapshot.username,
            allowSelfSigned: snapshot.allowSelfSigned,
            useTokenAuth: snapshot.useTokenAuth,
        )
    }

    private func restoreWithCredentials(
        serverURL: String, username: String, allowSelfSigned: Bool, useTokenAuth: Bool,
    ) async throws {
        // Try restoring from stored API token first (avoids 2FA prompt)
        if let token = try? await KeychainManager.shared.getToken(for: serverURL, username: username) {
            let tokenClient = ForgejoClient(
                serverURL: serverURL,
                username: username,
                token: token,
                allowSelfSignedCertificates: allowSelfSigned,
            )
            do {
                let user = try await tokenClient.fetchCurrentUser()
                client = tokenClient
                currentUser = user
                isAuthenticated = true
                return
            } catch {
                // Token is invalid/expired, only fall through to password if this is not a token-only instance
                if useTokenAuth {
                    throw SessionRestoreError.fromTokenValidationError(error)
                }
                // Otherwise fall through to password-based login below
            }
        }

        // Fall back to password-based login (creates a new token)
        guard !useTokenAuth else {
            throw SessionRestoreError.tokenExpired
        }
        let password = try await KeychainManager.shared.getPassword(for: serverURL, username: username)
        try await login(
            serverURL: serverURL, username: username,
            password: password, allowSelfSigned: allowSelfSigned,
        )
    }

    // Stub factories for SwiftUI previews
    #if DEBUG
        static func preview(user: User, instance: ForgejoInstance? = nil) -> AuthenticationService {
            let service = AuthenticationService()
            service.isAuthenticated = true
            service.currentUser = user
            service.currentInstance = instance
            service.client = ForgejoClient(
                serverURL: instance?.serverURL ?? "https://forgejo.example.com",
                username: user.login,
                token: "preview-token",
            )
            return service
        }
    #endif
}

enum SessionRestoreError: LocalizedError, Equatable {
    case tokenExpired
    case tokenPermissionDenied
    case serverUnavailable
    case serverNotFound
    case networkUnavailable
    case certificateError
    case invalidServerResponse
    case tokenValidationFailedHTTPStatus(Int)
    case tokenValidationFailed

    var errorDescription: String? {
        switch self {
        case .tokenExpired:
            "Your API token is expired or has been revoked. Please edit this instance and enter a new token."
        case .tokenPermissionDenied:
            "Your API token does not have permission to access this account. "
                + "Please edit this instance and enter a token with the required scopes."
        case .serverUnavailable:
            "The Forgejo server returned an error while validating your token. Please try again later."
        case .serverNotFound:
            "The Forgejo server could not be found. Please check the instance URL."
        case .networkUnavailable:
            "Forji could not reach the Forgejo server while validating your token. "
                + "Please check your connection and try again."
        case .certificateError:
            "Forji could not validate the server certificate. "
                + "Enable self-signed certificates if this instance uses one."
        case .invalidServerResponse:
            "The Forgejo server returned an invalid response while validating your token."
        case let .tokenValidationFailedHTTPStatus(statusCode):
            "Forji could not validate your API token (HTTP \(statusCode)). "
                + "Please edit this instance or try again later."
        case .tokenValidationFailed:
            "Forji could not validate your API token. Please edit this instance or try again later."
        }
    }

    static func fromTokenValidationError(_ error: Error) -> SessionRestoreError {
        if let sessionError = error as? SessionRestoreError {
            return sessionError
        }
        if let authError = error as? AuthenticationError {
            return fromAuthenticationError(authError)
        }
        if let serviceError = error as? ServiceError {
            return fromServiceError(serviceError)
        }
        if error is URLError {
            return .networkUnavailable
        }
        return .tokenValidationFailed
    }

    private static func fromAuthenticationError(_ error: AuthenticationError) -> SessionRestoreError {
        switch error {
        case .certificateError:
            .certificateError
        case .invalidResponse:
            .invalidServerResponse
        case .invalidURL:
            .serverNotFound
        case .invalidCredentials, .otpRequired, .basicAuthBlockedBySecurityKey, .serverNotFound, .unknownError:
            fromHTTPErrorCategory(error.httpErrorCategory, statusCode: error.httpStatusCode)
        }
    }

    private static func fromServiceError(_ error: ServiceError) -> SessionRestoreError {
        switch error {
        case .invalidURL:
            .serverNotFound
        case .invalidResponse, .decodingFailed:
            .invalidServerResponse
        case .httpError:
            fromHTTPErrorCategory(error.httpErrorCategory, statusCode: error.httpStatusCode)
        case .noActiveInstance:
            .tokenValidationFailed
        case .notMergeable, .mergeConflict:
            .tokenValidationFailed
        }
    }

    private static func fromHTTPErrorCategory(
        _ category: HTTPErrorCategory?,
        statusCode: Int?,
    ) -> SessionRestoreError {
        switch category {
        case .authentication:
            .tokenExpired
        case .permissionDenied:
            .tokenPermissionDenied
        case .notFound:
            .serverNotFound
        case .server:
            .serverUnavailable
        case .other:
            if let statusCode {
                .tokenValidationFailedHTTPStatus(statusCode)
            } else {
                .tokenValidationFailed
            }
        case nil:
            .tokenValidationFailed
        }
    }
}
