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

public struct DevinUsageFetcher: Sendable {
    private static let log = CodexBarLog.logger(LogCategories.provider(.devin))
    private static let baseURL = URL(string: "https://app.devin.ai")!
    private static let defaultUserAgent =
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"

    public struct RequestAuth: Sendable, Equatable {
        public let bearerToken: String
        public let organization: String?
        public let internalOrganizationID: String?
        public let sourceLabel: String

        public init(
            bearerToken: String,
            organization: String?,
            internalOrganizationID: String?,
            sourceLabel: String)
        {
            self.bearerToken = bearerToken
            self.organization = organization
            self.internalOrganizationID = internalOrganizationID
            self.sourceLabel = sourceLabel
        }
    }

    public let browserDetection: BrowserDetection

    public init(browserDetection: BrowserDetection) {
        self.browserDetection = browserDetection
    }

    public func fetch(
        bearerTokenOverride: String? = nil,
        organizationOverride: String? = nil,
        timeout: TimeInterval = 15,
        logger: ((String) -> Void)? = nil,
        now: Date = Date(),
        transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> DevinUsageSnapshot
    {
        let auths = try self.resolveAuths(
            bearerTokenOverride: bearerTokenOverride,
            organizationOverride: organizationOverride,
            logger: logger)
        var lastError: Error?
        for auth in auths {
            do {
                return try await Self.fetchQuotaUsage(
                    auth: auth,
                    organizationOverride: organizationOverride,
                    timeout: timeout,
                    logger: logger,
                    now: now,
                    transport: transport)
            } catch {
                lastError = error
                logger?("[devin] Session from \(auth.sourceLabel) failed: \(error.localizedDescription)")
                if auth.sourceLabel == "manual" || !Self.shouldTryNextSession(after: error) {
                    throw error
                }
            }
        }
        throw lastError ?? DevinUsageError.noSession
    }

    public static func fetchQuotaUsage(
        auth: RequestAuth,
        organizationOverride: String? = nil,
        timeout: TimeInterval = 15,
        logger: ((String) -> Void)? = nil,
        now: Date = Date(),
        transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> DevinUsageSnapshot
    {
        let organization = self.normalizedOrganization(organizationOverride) ??
            self.normalizedOrganization(auth.organization)
        guard let organization else {
            throw DevinUsageError.missingOrganization
        }

        var lastError: Error?
        for path in self.candidatePaths(
            organization: organization,
            internalOrganizationID: auth.internalOrganizationID)
        {
            let data: Data
            do {
                data = try await self.fetch(
                    path: path,
                    auth: auth,
                    timeout: timeout,
                    transport: transport)
            } catch {
                lastError = error
                logger?("[devin] /api/\(path) failed: \(error.localizedDescription)")
                if case DevinUsageError.invalidCredentials = error {
                    throw error
                }
                continue
            }
            logger?("[devin] Fetched quota usage from /api/\(path)")
            return try DevinUsageParser.parse(data, organization: organization, now: now)
        }

        throw lastError ?? DevinUsageError.apiError("No Devin quota endpoint succeeded.")
    }

    public static func manualAuth(from raw: String?, organization: String? = nil) -> RequestAuth? {
        guard var token = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty else {
            return nil
        }
        if token.lowercased().hasPrefix("authorization:") {
            token = token.dropHeaderName().trimmingCharacters(in: .whitespacesAndNewlines)
        }
        if token.lowercased().hasPrefix("bearer ") {
            token = String(token.dropFirst(7)).trimmingCharacters(in: .whitespacesAndNewlines)
        }
        guard !token.isEmpty else { return nil }
        return RequestAuth(
            bearerToken: token,
            organization: self.normalizedOrganization(organization),
            internalOrganizationID: self.internalOrganizationID(from: organization),
            sourceLabel: "manual")
    }

    private func resolveAuths(
        bearerTokenOverride: String?,
        organizationOverride: String?,
        logger: ((String) -> Void)?) throws -> [RequestAuth]
    {
        if let manual = Self.manualAuth(from: bearerTokenOverride, organization: organizationOverride) {
            logger?("[devin] Using manual Bearer token")
            return [manual]
        }

        #if os(macOS)
        let normalizedOrganizationOverride = Self.normalizedOrganization(organizationOverride)
        let sessions = DevinSessionImporter.importSessions(
            browserDetection: self.browserDetection,
            organizationOverride: normalizedOrganizationOverride,
            logger: logger)
        guard !sessions.isEmpty else {
            throw DevinUsageError.noSession
        }
        logger?("[devin] Found \(sessions.count) browser session(s)")
        return sessions.map { session in
            RequestAuth(
                bearerToken: session.accessToken,
                organization: normalizedOrganizationOverride ?? Self.normalizedOrganization(session.organization),
                internalOrganizationID: session.internalOrganizationID,
                sourceLabel: session.sourceLabel)
        }
        #else
        throw DevinUsageError.noSession
        #endif
    }

    static func shouldTryNextSession(after error: Error) -> Bool {
        switch error {
        case DevinUsageError.invalidCredentials, DevinUsageError.apiError, DevinUsageError.missingOrganization:
            true
        default:
            false
        }
    }

    private static func fetch(
        path: String,
        auth: RequestAuth,
        timeout: TimeInterval,
        transport: any ProviderHTTPTransport) async throws -> Data
    {
        let url = self.baseURL.appending(path: "api/\(path)")
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.timeoutInterval = timeout
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
        request.setValue(self.defaultUserAgent, forHTTPHeaderField: "User-Agent")
        request.setValue("Bearer \(auth.bearerToken)", forHTTPHeaderField: "Authorization")
        if let internalOrganizationID = auth.internalOrganizationID {
            request.setValue(internalOrganizationID, forHTTPHeaderField: "x-cog-org-id")
        }
        let response = try await transport.response(for: request)
        guard response.statusCode == 200 else {
            let body = String(data: response.data.prefix(200), encoding: .utf8) ?? "<binary>"
            if response.statusCode == 401 || response.statusCode == 403 {
                throw DevinUsageError.invalidCredentials
            }
            Self.log.error("Devin API returned \(response.statusCode): \(body)")
            throw DevinUsageError.apiError("HTTP \(response.statusCode)")
        }
        return response.data
    }

    private static func candidatePaths(organization: String, internalOrganizationID: String?) -> [String] {
        var paths: [String] = []
        let normalized = self.normalizedOrganization(organization) ?? organization
        if let internalOrganizationID {
            paths.append("\(internalOrganizationID)/billing/quota/usage")
        }
        paths.append("\(normalized)/billing/quota/usage")
        if normalized.hasPrefix("org/") {
            let slug = String(normalized.dropFirst(4))
            paths.append("\(slug)/billing/quota/usage")
        }
        if !normalized.hasPrefix("org/"), !normalized.hasPrefix("organizations/") {
            paths.append("org/\(normalized)/billing/quota/usage")
        }
        if let internalOrganizationID {
            paths.append("organizations/\(internalOrganizationID)/billing/quota/usage")
        }
        return paths.removingDuplicates()
    }

    public static func normalizedOrganization(_ raw: String?) -> String? {
        guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
            return nil
        }
        if let url = URL(string: value),
           let host = url.host?.lowercased(),
           host == "devin.ai" || host.hasSuffix(".devin.ai")
        {
            let components = url.path.split(separator: "/").map(String.init)
            if components.count >= 2, components[0] == "org" {
                value = "org/\(components[1])"
            } else if components.count >= 2, components[0] == "organizations" {
                value = "organizations/\(components[1])"
            }
        }
        value = value.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        if value.hasPrefix("org/") || value.hasPrefix("organizations/") {
            return value
        }
        if self.isInternalOrganizationID(value) {
            return "organizations/\(value)"
        }
        return "org/\(value)"
    }

    private static func internalOrganizationID(from raw: String?) -> String? {
        guard let normalized = self.normalizedOrganization(raw),
              normalized.hasPrefix("organizations/")
        else {
            return nil
        }
        return String(normalized.dropFirst("organizations/".count))
    }

    static func isInternalOrganizationID(_ value: String) -> Bool {
        value.hasPrefix("org-") || value.hasPrefix("org_")
    }
}

extension String {
    fileprivate func dropHeaderName() -> String {
        guard let index = self.firstIndex(of: ":") else { return self }
        return String(self[self.index(after: index)...])
    }
}

extension [String] {
    fileprivate func removingDuplicates() -> [String] {
        var seen = Set<String>()
        return self.filter { seen.insert($0).inserted }
    }
}
