import AppKit
import CodexBarCore
import Darwin
import Foundation
import Network

enum AntigravityLoginRunner {
    enum Phase {
        case waitingBrowser
    }

    struct Result {
        enum Outcome {
            case success(String?)
            case cancelled
            case timedOut
            case launchFailed(String)
            case failed(String)
        }

        let outcome: Outcome
    }

    static func run(
        timeout: TimeInterval = 120,
        onPhaseChange: (@Sendable (Phase) -> Void)? = nil,
        onCredentialsCreated: (@Sendable () -> Void)? = nil) async -> Result
    {
        guard let oauthClient = AntigravityOAuthConfig.resolvedClient() else {
            return Result(outcome: .failed(AntigravityOAuthConfig.missingCredentialsMessage))
        }

        let state = UUID().uuidString.replacingOccurrences(of: "-", with: "")
        let server = AntigravityLoopbackServer(state: state)

        do {
            let callbackURL = try await server.start()
            let authURL = try Self.makeAuthorizationURL(
                redirectURL: callbackURL,
                state: state,
                oauthClient: oauthClient)
            onPhaseChange?(.waitingBrowser)

            let opened = await MainActor.run {
                NSWorkspace.shared.open(authURL)
            }
            guard opened else {
                server.stop()
                return Result(outcome: .launchFailed(authURL.absoluteString))
            }

            let callback = try await withThrowingTaskGroup(of: AntigravityOAuthCallback.self) { group in
                group.addTask {
                    try await server.waitForCallback()
                }
                group.addTask {
                    try await Task.sleep(for: .seconds(timeout))
                    server.cancelCallbackWait(with: AntigravityLoginError.timedOut)
                    throw AntigravityLoginError.timedOut
                }
                defer { group.cancelAll() }
                return try await group.next().unsafelyUnwrapped
            }
            server.stop()

            if let error = callback.error?.trimmingCharacters(in: .whitespacesAndNewlines), !error.isEmpty {
                if error == "access_denied" {
                    return Result(outcome: .cancelled)
                }
                return Result(outcome: .failed(error))
            }

            guard callback.returnedState == state else {
                return Result(outcome: .failed("Google login state mismatch."))
            }
            guard let code = callback.code?.trimmingCharacters(in: .whitespacesAndNewlines), !code.isEmpty else {
                return Result(outcome: .failed("Google login did not return an authorization code."))
            }

            let tokenResponse = try await Self.exchangeCodeForTokens(
                code: code,
                redirectURL: callbackURL,
                oauthClient: oauthClient)
            let email = try await Self.fetchUserEmail(accessToken: tokenResponse.accessToken)
            let credentials = AntigravityOAuthCredentials(
                accessToken: tokenResponse.accessToken,
                refreshToken: tokenResponse.refreshToken,
                expiryDate: Date().addingTimeInterval(TimeInterval(tokenResponse.expiresIn)),
                idToken: tokenResponse.idToken,
                email: email,
                projectID: nil,
                clientID: oauthClient.clientID,
                clientSecret: oauthClient.clientSecret)
            try AntigravityOAuthCredentialsStore().save(credentials)
            onCredentialsCreated?()
            return Result(outcome: .success(email))
        } catch is CancellationError {
            server.stop()
            return Result(outcome: .cancelled)
        } catch AntigravityLoginError.timedOut {
            server.stop()
            return Result(outcome: .timedOut)
        } catch let AntigravityLoginError.launchFailed(message) {
            server.stop()
            return Result(outcome: .launchFailed(message))
        } catch {
            server.stop()
            return Result(outcome: .failed(error.localizedDescription))
        }
    }

    static func makeAuthorizationURL(
        redirectURL: URL,
        state: String,
        oauthClient: AntigravityOAuthClient) throws -> URL
    {
        guard var components = URLComponents(url: AntigravityOAuthConfig.authURL, resolvingAgainstBaseURL: false) else {
            throw AntigravityLoginError.invalidAuthorizationURL
        }
        components.queryItems = [
            URLQueryItem(name: "client_id", value: oauthClient.clientID),
            URLQueryItem(name: "redirect_uri", value: redirectURL.absoluteString),
            URLQueryItem(name: "response_type", value: "code"),
            URLQueryItem(name: "scope", value: AntigravityOAuthConfig.scopes.joined(separator: " ")),
            URLQueryItem(name: "access_type", value: "offline"),
            URLQueryItem(name: "prompt", value: "select_account consent"),
            URLQueryItem(name: "state", value: state),
        ]
        guard let url = components.url else {
            throw AntigravityLoginError.invalidAuthorizationURL
        }
        return url
    }

    private static func exchangeCodeForTokens(
        code: String,
        redirectURL: URL,
        oauthClient: AntigravityOAuthClient) async throws -> TokenResponse
    {
        var request = URLRequest(url: AntigravityOAuthConfig.tokenURL)
        request.httpMethod = "POST"
        request.timeoutInterval = 30
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = Self.formBody([
            "code": code,
            "client_id": oauthClient.clientID,
            "client_secret": oauthClient.clientSecret,
            "redirect_uri": redirectURL.absoluteString,
            "grant_type": "authorization_code",
        ])

        let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw AntigravityLoginError.failed("Invalid token response.")
        }
        guard httpResponse.statusCode == 200 else {
            let message = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
                ?? "HTTP \(httpResponse.statusCode)"
            throw AntigravityLoginError.failed(message)
        }
        do {
            return try JSONDecoder().decode(TokenResponse.self, from: data)
        } catch {
            throw AntigravityLoginError.failed("Could not decode token response.")
        }
    }

    private static func fetchUserEmail(accessToken: String) async throws -> String? {
        var request = URLRequest(url: AntigravityOAuthConfig.userInfoURL)
        request.timeoutInterval = 15
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

        do {
            let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                return nil
            }
            let userInfo = try JSONDecoder().decode(UserInfoResponse.self, from: data)
            let email = userInfo.email?.trimmingCharacters(in: .whitespacesAndNewlines)
            return (email?.isEmpty == false) ? email : nil
        } catch {
            return nil
        }
    }

    private static func formBody(_ values: [String: String]) -> Data? {
        values
            .map { key, value in
                let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? key
                let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? value
                return "\(encodedKey)=\(encodedValue)"
            }
            .joined(separator: "&")
            .data(using: .utf8)
    }
}

private enum AntigravityLoginError: LocalizedError {
    case invalidAuthorizationURL
    case timedOut
    case launchFailed(String)
    case failed(String)

    var errorDescription: String? {
        switch self {
        case .invalidAuthorizationURL:
            "Could not build the Antigravity login URL."
        case .timedOut:
            "Antigravity login timed out."
        case let .launchFailed(message):
            message
        case let .failed(message):
            message
        }
    }
}

private struct TokenResponse: Decodable {
    let accessToken: String
    let refreshToken: String?
    let expiresIn: Int
    let idToken: String?

    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
        case idToken = "id_token"
    }
}

private struct UserInfoResponse: Decodable {
    let email: String?
}

private struct AntigravityOAuthCallback {
    let code: String?
    let returnedState: String?
    let error: String?
}

private final class AntigravityLoopbackServer: @unchecked Sendable {
    private let expectedState: String
    private let queue = DispatchQueue(label: "codexbar.antigravity.oauth")
    private let lock = NSLock()
    private var listener: NWListener?
    private var readyContinuation: CheckedContinuation<URL, Error>?
    private var callbackContinuation: CheckedContinuation<AntigravityOAuthCallback, Error>?
    private var pendingCallbackResult: Result<AntigravityOAuthCallback, Error>?
    private var completed = false

    init(state: String) {
        self.expectedState = state
    }

    func start() async throws -> URL {
        let port = try Self.findAvailablePort()
        guard let endpointPort = NWEndpoint.Port(rawValue: port) else {
            throw AntigravityLoginError.failed("Could not reserve a local callback port.")
        }
        let listener = try NWListener(using: .tcp, on: endpointPort)
        self.listener = listener
        listener.newConnectionHandler = { [weak self] connection in
            self?.handle(connection)
        }

        return try await withCheckedThrowingContinuation { continuation in
            self.readyContinuation = continuation
            listener.stateUpdateHandler = { [weak self] state in
                guard let self else { return }
                switch state {
                case .ready:
                    let url = URL(string: "http://127.0.0.1:\(port)/callback")!
                    self.finishReady(with: .success(url))
                case let .failed(error):
                    self.finishReady(with: .failure(error))
                    self.finishCallback(with: .failure(error))
                default:
                    break
                }
            }
            listener.start(queue: self.queue)
        }
    }

    func waitForCallback() async throws -> AntigravityOAuthCallback {
        try await withCheckedThrowingContinuation { continuation in
            self.lock.lock()
            defer { self.lock.unlock() }
            if let pending = self.pendingCallbackResult {
                self.pendingCallbackResult = nil
                switch pending {
                case let .success(callback):
                    continuation.resume(returning: callback)
                case let .failure(error):
                    continuation.resume(throwing: error)
                }
                return
            }
            self.callbackContinuation = continuation
        }
    }

    func stop() {
        self.listener?.cancel()
        self.listener = nil
    }

    func cancelCallbackWait(with error: Error) {
        self.stop()
        self.finishCallback(with: .failure(error))
    }

    private func handle(_ connection: NWConnection) {
        connection.start(queue: self.queue)
        self.receive(on: connection, accumulated: Data())
    }

    private func receive(on connection: NWConnection, accumulated: Data) {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, error in
            guard let self else { return }
            if let error {
                self.finishCallback(with: .failure(error))
                connection.cancel()
                return
            }

            var buffer = accumulated
            if let data {
                buffer.append(data)
            }

            let headerMarker = Data("\r\n\r\n".utf8)
            if buffer.range(of: headerMarker) == nil, !isComplete {
                self.receive(on: connection, accumulated: buffer)
                return
            }

            let callback = self.parseCallback(from: buffer)
            let response = self.httpResponse(for: callback)
            connection.send(content: response, completion: .contentProcessed { _ in
                connection.cancel()
            })
            self.finishCallback(with: .success(callback))
        }
    }

    private func parseCallback(from data: Data) -> AntigravityOAuthCallback {
        guard let request = String(data: data, encoding: .utf8),
              let line = request.components(separatedBy: "\r\n").first
        else {
            return AntigravityOAuthCallback(code: nil, returnedState: nil, error: "Invalid callback request.")
        }

        let parts = line.split(separator: " ")
        guard parts.count >= 2,
              let url = URL(string: "http://127.0.0.1\(parts[1])"),
              let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        else {
            return AntigravityOAuthCallback(code: nil, returnedState: nil, error: "Invalid callback URL.")
        }

        let code = components.queryItems?.first(where: { $0.name == "code" })?.value
        let returnedState = components.queryItems?.first(where: { $0.name == "state" })?.value
        let error = components.queryItems?.first(where: { $0.name == "error" })?.value

        guard components.path == "/callback" else {
            return AntigravityOAuthCallback(code: nil, returnedState: returnedState, error: "Unexpected callback path.")
        }
        if let returnedState, returnedState != self.expectedState {
            return AntigravityOAuthCallback(code: code, returnedState: returnedState, error: "State mismatch.")
        }
        return AntigravityOAuthCallback(code: code, returnedState: returnedState, error: error)
    }

    private func httpResponse(for callback: AntigravityOAuthCallback) -> Data {
        let success = callback.error == nil && callback.code?.isEmpty == false
        let status = success ? "200 OK" : "400 Bad Request"
        let title = success ? L("Login Successful") : L("Login Failed")
        let detail = success
            ? L("You can close this window and return to CodexBar.")
            : L("You can close this window and try again.")
        let html = """
        <html>
          <body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 32px; text-align: center;">
            <h1>\(title)</h1>
            <p>\(detail)</p>
          </body>
        </html>
        """
        let body = Data(html.utf8)
        let header = """
        HTTP/1.1 \(status)\r
        Content-Type: text/html; charset=utf-8\r
        Content-Length: \(body.count)\r
        Connection: close\r
        \r
        """
        var response = Data(header.utf8)
        response.append(body)
        return response
    }

    private func finishReady(with result: Result<URL, Error>) {
        self.lock.lock()
        let continuation = self.readyContinuation
        self.readyContinuation = nil
        self.lock.unlock()
        switch result {
        case let .success(url):
            continuation?.resume(returning: url)
        case let .failure(error):
            continuation?.resume(throwing: error)
        }
    }

    private func finishCallback(with result: Result<AntigravityOAuthCallback, Error>) {
        self.lock.lock()
        guard !self.completed else {
            self.lock.unlock()
            return
        }
        self.completed = true
        let continuation = self.callbackContinuation
        self.callbackContinuation = nil
        if continuation == nil {
            self.pendingCallbackResult = result
        }
        self.lock.unlock()
        guard let continuation else { return }
        switch result {
        case let .success(callback):
            continuation.resume(returning: callback)
        case let .failure(error):
            continuation.resume(throwing: error)
        }
    }

    private static func findAvailablePort() throws -> UInt16 {
        let socketFD = socket(AF_INET, Int32(SOCK_STREAM), 0)
        guard socketFD >= 0 else {
            throw AntigravityLoginError.failed("Could not create a local callback socket.")
        }
        defer { close(socketFD) }

        var value: Int32 = 1
        setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout<Int32>.size))

        var address = sockaddr_in()
        address.sin_len = UInt8(MemoryLayout<sockaddr_in>.stride)
        address.sin_family = sa_family_t(AF_INET)
        address.sin_port = in_port_t(0).bigEndian
        address.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))

        let bindResult = withUnsafePointer(to: &address) { pointer in
            pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
                bind(socketFD, sockaddrPointer, socklen_t(MemoryLayout<sockaddr_in>.stride))
            }
        }
        guard bindResult == 0 else {
            throw AntigravityLoginError.failed("Could not bind a local callback port.")
        }

        var boundAddress = sockaddr_in()
        var length = socklen_t(MemoryLayout<sockaddr_in>.stride)
        let nameResult = withUnsafeMutablePointer(to: &boundAddress) { pointer in
            pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
                getsockname(socketFD, sockaddrPointer, &length)
            }
        }
        guard nameResult == 0 else {
            throw AntigravityLoginError.failed("Could not inspect the callback port.")
        }
        return UInt16(bigEndian: boundAddress.sin_port)
    }
}

extension CharacterSet {
    fileprivate static let urlQueryValueAllowed: CharacterSet = {
        var allowed = CharacterSet.urlQueryAllowed
        allowed.remove(charactersIn: "+&=")
        return allowed
    }()
}
