import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

// MARK: - Models

public struct ZedAuthenticatedUserResponse: Decodable, Equatable, Sendable {
    public let user: ZedAuthenticatedUser
    public let plan: ZedPlanInfo

    public init(user: ZedAuthenticatedUser, plan: ZedPlanInfo) {
        self.user = user
        self.plan = plan
    }
}

public struct ZedAuthenticatedUser: Decodable, Equatable, Sendable {
    public let id: Int
    public let githubLogin: String
    public let name: String?

    enum CodingKeys: String, CodingKey {
        case id
        case githubLogin = "github_login"
        case name
    }
}

public struct ZedPlanInfo: Decodable, Equatable, Sendable {
    public let planV3: String
    public let subscriptionPeriod: ZedSubscriptionPeriod?
    public let usage: ZedCurrentUsage
    public let hasOverdueInvoices: Bool

    enum CodingKeys: String, CodingKey {
        case planV3 = "plan_v3"
        case subscriptionPeriod = "subscription_period"
        case usage
        case hasOverdueInvoices = "has_overdue_invoices"
    }
}

public struct ZedSubscriptionPeriod: Decodable, Equatable, Sendable {
    public let startedAt: Date
    public let endedAt: Date

    enum CodingKeys: String, CodingKey {
        case startedAt = "started_at"
        case endedAt = "ended_at"
    }
}

public struct ZedCurrentUsage: Decodable, Equatable, Sendable {
    public let editPredictions: ZedUsageData

    enum CodingKeys: String, CodingKey {
        case editPredictions = "edit_predictions"
    }
}

public struct ZedUsageData: Decodable, Equatable, Sendable {
    public let used: Int
    public let limit: ZedUsageLimit
}

public enum ZedUsageLimit: Equatable, Sendable {
    case limited(Int)
    case unlimited
}

extension ZedUsageLimit: Decodable {
    public init(from decoder: Decoder) throws {
        if let single = try? decoder.singleValueContainer() {
            if let string = try? single.decode(String.self), string == "unlimited" {
                self = .unlimited
                return
            }
            if let value = try? single.decode(Int.self) {
                self = .limited(value)
                return
            }
        }

        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(Int.self, forKey: .limited) {
            self = .limited(value)
            return
        }

        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unrecognized Zed usage limit"))
    }

    private enum CodingKeys: String, CodingKey {
        case limited
    }
}

public struct ZedCredentials: Equatable, Sendable {
    public let userID: String
    public let accessToken: String

    public init(userID: String, accessToken: String) {
        self.userID = userID
        self.accessToken = accessToken
    }

    public var authorizationHeader: String {
        "\(self.userID) \(self.accessToken)"
    }
}

public struct ZedUsageSnapshot: Sendable, Equatable {
    public let response: ZedAuthenticatedUserResponse
    public let updatedAt: Date

    public init(response: ZedAuthenticatedUserResponse, updatedAt: Date = Date()) {
        self.response = response
        self.updatedAt = updatedAt
    }
}

// MARK: - Errors

public enum ZedStatusProbeError: LocalizedError, Sendable, Equatable {
    case notSupported
    case notSignedIn
    case keychainUnavailable
    case invalidServerURL(String)
    case untrustedServerConfiguration
    case networkError(String)
    case httpError(Int)
    case unauthorized
    case parseFailed(String)

    public var errorDescription: String? {
        switch self {
        case .notSupported:
            "Zed is only supported on macOS."
        case .notSignedIn:
            "Not signed in to Zed. Sign in from the Zed editor app with GitHub."
        case .keychainUnavailable:
            "Could not read Zed credentials from the Keychain. Grant CodexBar Keychain access or sign in to Zed again."
        case let .invalidServerURL(value):
            "Zed server URL is invalid: \(value)"
        case .untrustedServerConfiguration:
            "Zed custom servers must use HTTPS and store credentials under the same server URL."
        case let .networkError(message):
            "Zed cloud API request failed: \(message)"
        case let .httpError(status):
            "Zed cloud API returned HTTP \(status)."
        case .unauthorized:
            "Zed credentials are invalid or expired. Sign in to Zed again."
        case let .parseFailed(message):
            "Could not parse Zed account response: \(message)"
        }
    }
}

// MARK: - Settings

public struct ZedClientSettings: Sendable, Equatable {
    public let credentialsURL: String?
    public let serverURL: String?

    public init(credentialsURL: String?, serverURL: String?) {
        self.credentialsURL = credentialsURL
        self.serverURL = serverURL
    }

    public var keychainServiceURL: String {
        let trimmedCredentials = self.credentialsURL?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let trimmedCredentials, !trimmedCredentials.isEmpty {
            return trimmedCredentials
        }
        let trimmedServer = self.serverURL?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let trimmedServer, !trimmedServer.isEmpty {
            return trimmedServer
        }
        return ZedStatusProbe.defaultKeychainServiceURL
    }

    public var cloudAPIURL: URL? {
        let trimmedServer = self.serverURL?.trimmingCharacters(in: .whitespacesAndNewlines)
        let server = if let trimmedServer, !trimmedServer.isEmpty {
            trimmedServer
        } else {
            ZedStatusProbe.defaultKeychainServiceURL
        }
        let isTrustedZedServer = server == "https://zed.dev" || server == "https://staging.zed.dev"
        let trimmedCredentials = self.credentialsURL?.trimmingCharacters(in: .whitespacesAndNewlines)
        if !isTrustedZedServer,
           let trimmedCredentials,
           !trimmedCredentials.isEmpty,
           trimmedCredentials != server
        {
            return nil
        }
        let cloudBase = switch server {
        case "https://zed.dev", "https://staging.zed.dev":
            "https://cloud.zed.dev"
        default:
            server
        }
        guard let baseURL = URL(string: cloudBase),
              let scheme = baseURL.scheme?.lowercased(),
              scheme == "https",
              baseURL.host != nil
        else {
            return nil
        }
        return baseURL.appendingPathComponent("client/users/me")
    }

    public static func load(from url: URL = ZedStatusProbe.defaultSettingsURL) -> ZedClientSettings? {
        guard let data = try? Data(contentsOf: url) else { return nil }
        struct Payload: Decodable {
            let credentialsURL: String?
            let serverURL: String?

            enum CodingKeys: String, CodingKey {
                case credentialsURL = "credentials_url"
                case serverURL = "server_url"
            }
        }
        guard let payload = try? JSONDecoder().decode(Payload.self, from: data) else { return nil }
        return ZedClientSettings(
            credentialsURL: payload.credentialsURL,
            serverURL: payload.serverURL)
    }
}

// MARK: - Credentials

public protocol ZedCredentialsReading: Sendable {
    func loadCredentials(serviceURL: String) throws -> ZedCredentials?
}

#if os(macOS)
import Security

public struct ZedKeychainCredentialsReader: ZedCredentialsReading, Sendable {
    public init() {}

    public func loadCredentials(serviceURL: String) throws -> ZedCredentials? {
        if let credentials = try self.loadInternetPasswordCredentials(server: serviceURL) {
            return credentials
        }
        return try self.loadGenericPasswordCredentials(service: serviceURL)
    }

    private func loadInternetPasswordCredentials(server: String) throws -> ZedCredentials? {
        var query: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword,
            kSecAttrServer as String: server,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnAttributes as String: true,
            kSecReturnData as String: true,
        ]
        KeychainNoUIQuery.apply(to: &query)
        return try self.credentials(from: query)
    }

    private func loadGenericPasswordCredentials(service: String) throws -> ZedCredentials? {
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnAttributes as String: true,
            kSecReturnData as String: true,
        ]
        KeychainNoUIQuery.apply(to: &query)
        return try self.credentials(from: query)
    }

    private func credentials(from query: [String: Any]) throws -> ZedCredentials? {
        var result: AnyObject?
        let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
        switch status {
        case errSecSuccess:
            break
        case errSecItemNotFound:
            return nil
        case errSecInteractionNotAllowed, errSecAuthFailed, errSecNoAccessForItem:
            throw ZedStatusProbeError.keychainUnavailable
        default:
            throw ZedStatusProbeError.keychainUnavailable
        }

        guard let item = result as? [String: Any],
              let account = item[kSecAttrAccount as String] as? String,
              !account.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        else {
            return nil
        }

        let tokenData: Data? = if let data = item[kSecValueData as String] as? Data {
            data
        } else {
            nil
        }
        guard let tokenData,
              let accessToken = String(data: tokenData, encoding: .utf8),
              !accessToken.isEmpty
        else {
            return nil
        }

        return ZedCredentials(userID: account, accessToken: accessToken)
    }
}
#else
public struct ZedKeychainCredentialsReader: ZedCredentialsReading, Sendable {
    public init() {}

    public func loadCredentials(serviceURL _: String) throws -> ZedCredentials? {
        throw ZedStatusProbeError.notSupported
    }
}
#endif

// MARK: - Probe

public struct ZedStatusProbe: Sendable {
    public static let defaultKeychainServiceURL = "https://zed.dev"
    public static let cloudAPIURL = URL(string: "https://cloud.zed.dev/client/users/me")!

    public static var defaultSettingsURL: URL {
        let home = FileManager.default.homeDirectoryForCurrentUser
        return home
            .appendingPathComponent(".config/zed/settings.json")
    }

    private static let logger = CodexBarLog.logger(LogCategories.provider(.zed))

    private let credentialsReader: any ZedCredentialsReading
    private let transport: any ProviderHTTPTransport
    private let settingsLoader: @Sendable () -> ZedClientSettings?

    public init(
        credentialsReader: any ZedCredentialsReading = ZedKeychainCredentialsReader(),
        transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
        settingsLoader: @escaping @Sendable () -> ZedClientSettings? = { ZedClientSettings.load() })
    {
        self.credentialsReader = credentialsReader
        self.transport = transport
        self.settingsLoader = settingsLoader
    }

    public func fetch() async throws -> ZedUsageSnapshot {
        let settings = self.settingsLoader()
        let serviceURL = settings?.keychainServiceURL ?? Self.defaultKeychainServiceURL
        let cloudAPIURL: URL
        if let settings {
            guard let configuredURL = settings.cloudAPIURL else {
                let serverURL = settings.serverURL ?? ""
                guard URL(string: serverURL)?.scheme?.lowercased() == "https" else {
                    throw ZedStatusProbeError.invalidServerURL(serverURL)
                }
                throw ZedStatusProbeError.untrustedServerConfiguration
            }
            cloudAPIURL = configuredURL
        } else {
            cloudAPIURL = Self.cloudAPIURL
        }
        guard let credentials = try self.credentialsReader.loadCredentials(serviceURL: serviceURL) else {
            throw ZedStatusProbeError.notSignedIn
        }

        let response = try await self.fetchAuthenticatedUser(credentials: credentials, apiURL: cloudAPIURL)
        return ZedUsageSnapshot(response: response)
    }

    private func fetchAuthenticatedUser(
        credentials: ZedCredentials,
        apiURL: URL) async throws -> ZedAuthenticatedUserResponse
    {
        var request = URLRequest(url: apiURL)
        request.httpMethod = "GET"
        request.setValue(credentials.authorizationHeader, forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Accept")

        let httpResponse: ProviderHTTPResponse
        do {
            httpResponse = try await self.transport.response(for: request)
        } catch is CancellationError {
            throw CancellationError()
        } catch let error as URLError where error.code == .cancelled {
            throw CancellationError()
        } catch {
            Self.logger.debug("Zed cloud API transport failed: \(error.localizedDescription)")
            throw ZedStatusProbeError.networkError(error.localizedDescription)
        }

        switch httpResponse.statusCode {
        case 200:
            return try Self.parseResponse(httpResponse.data)
        case 401, 403:
            throw ZedStatusProbeError.unauthorized
        default:
            throw ZedStatusProbeError.httpError(httpResponse.statusCode)
        }
    }

    public static func parseResponse(_ data: Data) throws -> ZedAuthenticatedUserResponse {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom { decoder in
            let container = try decoder.singleValueContainer()
            let value = try container.decode(String.self)
            if let date = Self.parseISO8601Date(value) {
                return date
            }
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Invalid ISO8601 date: \(value)")
        }
        do {
            return try decoder.decode(ZedAuthenticatedUserResponse.self, from: data)
        } catch {
            throw ZedStatusProbeError.parseFailed(error.localizedDescription)
        }
    }

    private static func parseISO8601Date(_ value: String) -> Date? {
        let withFractional = ISO8601DateFormatter()
        withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = withFractional.date(from: value) {
            return date
        }
        let plain = ISO8601DateFormatter()
        plain.formatOptions = [.withInternetDateTime]
        return plain.date(from: value)
    }
}

// MARK: - UsageSnapshot mapping

extension ZedUsageSnapshot {
    public func toUsageSnapshot() -> UsageSnapshot {
        let plan = self.response.plan
        let user = self.response.user

        let primary = Self.makeEditPredictionsWindow(
            used: plan.usage.editPredictions.used,
            limit: plan.usage.editPredictions.limit)

        let secondary = plan.subscriptionPeriod.map { period in
            RateWindow(
                usedPercent: Self.billingCycleUsedPercent(startedAt: period.startedAt, endedAt: period.endedAt),
                windowMinutes: nil,
                resetsAt: period.endedAt,
                resetDescription: Self.formatResetDescription(period.endedAt))
        }

        var extraRateWindows: [NamedRateWindow] = []
        if plan.hasOverdueInvoices {
            extraRateWindows.append(NamedRateWindow(
                id: "zed.overdue-invoices",
                title: "Billing",
                window: RateWindow(
                    usedPercent: 100,
                    windowMinutes: nil,
                    resetsAt: nil,
                    resetDescription: "Overdue invoices"),
                usageKnown: false))
        }

        let identity = ProviderIdentitySnapshot(
            providerID: .zed,
            accountEmail: user.githubLogin.nilIfEmpty,
            accountOrganization: user.name?.nilIfEmpty,
            loginMethod: Self.displayPlanName(plan.planV3))

        return UsageSnapshot(
            primary: primary,
            secondary: secondary,
            extraRateWindows: extraRateWindows.isEmpty ? nil : extraRateWindows,
            subscriptionRenewsAt: plan.subscriptionPeriod?.endedAt,
            updatedAt: self.updatedAt,
            identity: identity)
    }

    private static func makeEditPredictionsWindow(used: Int, limit: ZedUsageLimit) -> RateWindow? {
        switch limit {
        case .unlimited:
            return RateWindow(
                usedPercent: 0,
                windowMinutes: nil,
                resetsAt: nil,
                resetDescription: "Unlimited")
        case let .limited(total):
            guard total > 0 else { return nil }
            let clampedUsed = max(0, min(total, used))
            let usedPercent = Double(clampedUsed) / Double(total) * 100.0
            return RateWindow(
                usedPercent: usedPercent,
                windowMinutes: nil,
                resetsAt: nil,
                resetDescription: "\(clampedUsed) / \(total) predictions")
        }
    }

    public static func displayPlanName(_ rawPlan: String) -> String {
        switch rawPlan.lowercased() {
        case "zed_free": "Zed Free"
        case "zed_pro": "Zed Pro"
        case "zed_pro_trial": "Zed Pro Trial"
        case "zed_student": "Zed Student"
        case "zed_business": "Zed Business"
        default:
            rawPlan
                .replacingOccurrences(of: "_", with: " ")
                .split(separator: " ")
                .map { word in
                    word.prefix(1).uppercased() + word.dropFirst().lowercased()
                }
                .joined(separator: " ")
        }
    }

    private static func billingCycleUsedPercent(startedAt: Date, endedAt: Date) -> Double {
        let now = Date()
        let total = endedAt.timeIntervalSince(startedAt)
        guard total > 0 else { return 0 }
        let elapsed = now.timeIntervalSince(startedAt)
        return max(0, min(100, elapsed / total * 100))
    }

    static func formatResetDescription(_ date: Date, now: Date = Date()) -> String? {
        let interval = date.timeIntervalSince(now)
        guard interval > 0 else { return "Cycle ended" }

        let hours = Int(interval / 3600)
        let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)

        if hours >= 24 {
            let days = hours / 24
            let remainingHours = hours % 24
            return "Cycle ends in \(days)d \(remainingHours)h"
        } else if hours > 0 {
            return "Cycle ends in \(hours)h \(minutes)m"
        } else {
            return "Cycle ends in \(minutes)m"
        }
    }
}

extension String {
    fileprivate var nilIfEmpty: String? {
        self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
    }
}
