import CodexBarCore
import Foundation

struct PreparedIdentity: Equatable {
    let email: String?
    let identity: CodexIdentity
    let providerAccountID: String?
    let workspaceLabel: String?
    let workspaceAccountID: String?
}

struct PreparedAuthMaterial {
    let homeURL: URL
    let rawData: Data
    let credentials: CodexOAuthCredentials
    let runtimeAccount: CodexAuthBackedAccount
    let authIdentity: PreparedIdentity
}

enum PreparedManagedHomeState {
    case readable(PreparedAuthMaterial)
    case missing(homeURL: URL)
    case unreadable(homeURL: URL)
}

struct PreparedStoredManagedAccount {
    let persisted: ManagedCodexAccount
    let persistedIdentity: PreparedIdentity
    let homeState: PreparedManagedHomeState

    var authIdentity: PreparedIdentity? {
        switch self.homeState {
        case let .readable(authMaterial):
            authMaterial.authIdentity
        case .missing, .unreadable:
            nil
        }
    }
}

enum PreparedLiveHomeState {
    case missing(homeURL: URL)
    case unreadable(homeURL: URL)
    case apiKeyOnly(PreparedAuthMaterial)
    case readable(PreparedAuthMaterial)
}

struct PreparedLiveAccount {
    let homeState: PreparedLiveHomeState

    var homeURL: URL {
        switch self.homeState {
        case let .missing(homeURL), let .unreadable(homeURL):
            homeURL
        case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
            authMaterial.homeURL
        }
    }

    var authIdentity: PreparedIdentity? {
        switch self.homeState {
        case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
            authMaterial.authIdentity
        case .missing, .unreadable:
            nil
        }
    }
}

struct PreparedPromotionContext {
    let snapshot: CodexAccountReconciliationSnapshot
    let managedAccounts: ManagedCodexAccountSet
    let storedManagedAccounts: [PreparedStoredManagedAccount]
    let target: PreparedStoredManagedAccount
    let live: PreparedLiveAccount
}

@MainActor
struct PreparedPromotionContextBuilder {
    private let store: any ManagedCodexAccountStoring
    private let workspaceResolver: any ManagedCodexWorkspaceResolving
    private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
    private let authMaterialReader: any CodexAuthMaterialReading
    private let baseEnvironment: [String: String]
    private let fileManager: FileManager

    init(
        store: any ManagedCodexAccountStoring,
        workspaceResolver: any ManagedCodexWorkspaceResolving,
        snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
        authMaterialReader: any CodexAuthMaterialReading,
        baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
        fileManager: FileManager = .default)
    {
        self.store = store
        self.workspaceResolver = workspaceResolver
        self.snapshotLoader = snapshotLoader
        self.authMaterialReader = authMaterialReader
        self.baseEnvironment = baseEnvironment
        self.fileManager = fileManager
    }

    func build(targetID: UUID) async throws -> PreparedPromotionContext {
        let snapshot = self.snapshotLoader.loadSnapshot()
        let managedAccounts = try self.store.loadAccounts()
        var preparedAccounts: [PreparedStoredManagedAccount] = []
        preparedAccounts.reserveCapacity(managedAccounts.accounts.count)
        for account in managedAccounts.accounts {
            let preparedAccount = try await self.prepareStoredManagedAccount(account)
            preparedAccounts.append(preparedAccount)
        }

        guard let target = preparedAccounts.first(where: { $0.persisted.id == targetID }) else {
            throw CodexAccountPromotionError.targetManagedAccountNotFound
        }

        let live = await self.prepareLiveAccount()
        return PreparedPromotionContext(
            snapshot: snapshot,
            managedAccounts: managedAccounts,
            storedManagedAccounts: preparedAccounts,
            target: target,
            live: live)
    }

    private func prepareStoredManagedAccount(
        _ account: ManagedCodexAccount) async throws
        -> PreparedStoredManagedAccount
    {
        let homeURL = URL(fileURLWithPath: account.managedHomePath, isDirectory: true)
        let persistedIdentity = Self.persistedIdentity(from: account)
        let homeState = await self.prepareManagedHomeState(homeURL: homeURL)

        return PreparedStoredManagedAccount(
            persisted: account,
            persistedIdentity: persistedIdentity,
            homeState: homeState)
    }

    private func prepareManagedHomeState(homeURL: URL) async -> PreparedManagedHomeState {
        let readResult = self.readAuthData(homeURL: homeURL)
        switch readResult {
        case .missing:
            return .missing(homeURL: homeURL)
        case .unreadable:
            return .unreadable(homeURL: homeURL)
        case let .readable(rawData):
            guard let authMaterial = await self.inspectAuthMaterial(homeURL: homeURL, rawData: rawData) else {
                return .unreadable(homeURL: homeURL)
            }
            return .readable(authMaterial)
        }
    }

    private func prepareLiveAccount() async -> PreparedLiveAccount {
        let liveHomeURL = self.liveHomeURL()
        let readResult = self.readAuthData(homeURL: liveHomeURL)
        switch readResult {
        case .missing:
            return PreparedLiveAccount(homeState: .missing(homeURL: liveHomeURL))
        case .unreadable:
            return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
        case let .readable(rawData):
            guard let authMaterial = await self.inspectAuthMaterial(homeURL: liveHomeURL, rawData: rawData) else {
                return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
            }
            if Self.isAPIKeyOnly(credentials: authMaterial.credentials, rawData: authMaterial.rawData) {
                return PreparedLiveAccount(homeState: .apiKeyOnly(authMaterial))
            }
            return PreparedLiveAccount(homeState: .readable(authMaterial))
        }
    }

    private func inspectAuthMaterial(homeURL: URL, rawData: Data) async -> PreparedAuthMaterial? {
        guard let credentials = try? CodexOAuthCredentialsStore.parse(data: rawData),
              let runtimeAccount = try? Self.runtimeAccount(from: rawData)
        else {
            return nil
        }

        let authIdentity = await self.derivedIdentity(
            homePath: homeURL.path,
            runtimeAccount: runtimeAccount)

        return PreparedAuthMaterial(
            homeURL: homeURL,
            rawData: rawData,
            credentials: credentials,
            runtimeAccount: runtimeAccount,
            authIdentity: authIdentity)
    }

    private func derivedIdentity(homePath: String, runtimeAccount: CodexAuthBackedAccount) async -> PreparedIdentity {
        let normalizedEmail = Self.normalizeEmail(runtimeAccount.email)
        let normalizedIdentity = Self.normalizedIdentity(runtimeAccount.identity, email: normalizedEmail)
        let providerAccountID: String? = switch normalizedIdentity {
        case let .providerAccount(id):
            ManagedCodexAccount.normalizeProviderAccountID(id)
        case .emailOnly, .unresolved:
            nil
        }
        let workspaceIdentity: CodexOpenAIWorkspaceIdentity? = if let providerAccountID {
            await self.workspaceResolver.resolveWorkspaceIdentity(
                homePath: homePath,
                providerAccountID: providerAccountID)
        } else {
            nil
        }

        return PreparedIdentity(
            email: normalizedEmail,
            identity: normalizedIdentity,
            providerAccountID: providerAccountID,
            workspaceLabel: workspaceIdentity?.workspaceLabel,
            workspaceAccountID: workspaceIdentity?.workspaceAccountID ?? providerAccountID)
    }

    private static func persistedIdentity(from account: ManagedCodexAccount) -> PreparedIdentity {
        let normalizedEmail = Self.normalizeEmail(account.email)
        let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(account.providerAccountID)
        let identity = Self.normalizedIdentity(
            CodexIdentityResolver.resolve(accountId: providerAccountID, email: normalizedEmail),
            email: normalizedEmail)

        return PreparedIdentity(
            email: normalizedEmail,
            identity: identity,
            providerAccountID: providerAccountID,
            workspaceLabel: account.workspaceLabel,
            workspaceAccountID: account.workspaceAccountID)
    }

    private func liveHomeURL() -> URL {
        CodexHomeScope.ambientHomeURL(env: self.baseEnvironment, fileManager: self.fileManager)
    }

    private func readAuthData(homeURL: URL) -> PreparedAuthReadState {
        do {
            let rawData = try self.authMaterialReader.readAuthData(homeURL: homeURL)
            guard let rawData else {
                return .missing
            }
            return .readable(rawData)
        } catch {
            return .unreadable
        }
    }

    private static func runtimeAccount(from rawData: Data) throws -> CodexAuthBackedAccount {
        guard let json = try JSONSerialization.jsonObject(with: rawData) as? [String: Any] else {
            throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
        }

        let tokens = json["tokens"] as? [String: Any]
        let idToken = tokens.flatMap {
            Self.nonEmptyString(in: $0, snakeCaseKey: "id_token", camelCaseKey: "idToken")
        }
        let payload = idToken.flatMap(UsageFetcher.parseJWT)
        let authDict = payload?["https://api.openai.com/auth"] as? [String: Any]
        let profileDict = payload?["https://api.openai.com/profile"] as? [String: Any]

        let email = Self.normalizeEmail(
            (payload?["email"] as? String) ?? (profileDict?["email"] as? String))
        let plan = Self.normalizedField(
            (authDict?["chatgpt_plan_type"] as? String) ?? (payload?["chatgpt_plan_type"] as? String))
        let accountID = ManagedCodexAccount.normalizeProviderAccountID(
            tokens.flatMap {
                Self.nonEmptyString(in: $0, snakeCaseKey: "account_id", camelCaseKey: "accountId")
            }
                ?? (authDict?["chatgpt_account_id"] as? String)
                ?? (payload?["chatgpt_account_id"] as? String))
        let identity = Self.normalizedIdentity(
            CodexIdentityResolver.resolve(accountId: accountID, email: email),
            email: email)

        return CodexAuthBackedAccount(identity: identity, email: email, plan: plan)
    }

    private static func normalizedField(_ value: String?) -> String? {
        guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
            return nil
        }
        return value
    }

    private static func normalizeEmail(_ email: String?) -> String? {
        CodexIdentityResolver.normalizeEmail(email)
    }

    private static func normalizedIdentity(_ identity: CodexIdentity, email: String?) -> CodexIdentity {
        guard let email else { return identity }
        return CodexIdentityMatcher.normalized(identity, fallbackEmail: email)
    }

    private static func isAPIKeyOnly(credentials: CodexOAuthCredentials, rawData: Data) -> Bool {
        guard self.hasUsableOAuthTokens(in: rawData) == false else {
            return false
        }
        return credentials.refreshToken.isEmpty
            && credentials.idToken == nil
            && credentials.accountId == nil
            && credentials.lastRefresh == nil
    }

    private static func hasUsableOAuthTokens(in rawData: Data) -> Bool {
        guard let json = try? JSONSerialization.jsonObject(with: rawData) as? [String: Any],
              let tokens = json["tokens"] as? [String: Any]
        else {
            return false
        }
        let accessToken = self.nonEmptyString(
            in: tokens,
            snakeCaseKey: "access_token",
            camelCaseKey: "accessToken")
        let refreshToken = self.nonEmptyString(
            in: tokens,
            snakeCaseKey: "refresh_token",
            camelCaseKey: "refreshToken")
        return accessToken != nil && refreshToken != nil
    }

    private static func nonEmptyString(
        in dictionary: [String: Any],
        snakeCaseKey: String,
        camelCaseKey: String)
        -> String?
    {
        if let value = dictionary[snakeCaseKey] as? String,
           value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
        {
            return value
        }
        if let value = dictionary[camelCaseKey] as? String,
           value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
        {
            return value
        }
        return nil
    }
}

private enum PreparedAuthReadState {
    case missing
    case unreadable
    case readable(Data)
}
