import Foundation

/// URLSession delegate that funnels every request lifecycle event
/// through ``DiagnosticLogger``. Drop it onto any `URLSession` whose
/// traffic we want forensically captured — primarily
/// ``RouteExplorerClient`` (where we need to see the exact 403 +
/// `reason:"clearance"` body) and ``FlightAwareScheduleClient``
/// (where we need to see if FA ever rate-limits us).
///
/// We never capture full response bodies — those can be 500 KB+ for
/// FA's trackpoll pages and would balloon the log file. The client
/// itself can log a body excerpt explicitly with `DiagnosticLogger`
/// after parsing, if needed.
///
/// Headers are filtered to a small forensically-useful subset — the
/// CDN/edge headers Cloudflare/Vercel use to identify themselves and
/// the cookies they set. We deliberately drop the giant
/// `Set-Cookie` body sometimes seen on Vercel responses so the log
/// stays scannable.
final class LoggingURLSessionDelegate: NSObject, URLSessionTaskDelegate {

    /// Label that gets prefixed onto every event for this delegate's
    /// session, so a single shared log can disambiguate which client
    /// the event came from (e.g. `RE`, `FA`, `BLOB`).
    let tag: String

    init(tag: String) {
        self.tag = tag
        super.init()
    }

    // MARK: - URLSessionTaskDelegate

    /// Fired right before the request goes on the wire (post any
    /// redirect resolution). Capture method + URL + key headers so
    /// we can confirm e.g. the UA + Referer the client thinks it
    /// sent are the ones that actually went out.
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willBeginDelayedRequest request: URLRequest,
        completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void
    ) {
        logRequest("delayedRequest", request: request, taskID: task.taskIdentifier)
        completionHandler(.continueLoading, request)
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        DiagnosticLogger.shared.log("NET-\(tag)", "authChallenge", [
            "taskID": task.taskIdentifier,
            "method": challenge.protectionSpace.authenticationMethod,
            "host": challenge.protectionSpace.host,
        ])
        completionHandler(.performDefaultHandling, nil)
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        if let error {
            DiagnosticLogger.shared.log("NET-\(tag)", "didCompleteWithError", [
                "taskID": task.taskIdentifier,
                "error": error.localizedDescription,
                "code": (error as NSError).code,
                "domain": (error as NSError).domain,
            ])
            return
        }
        // No error → log the final response status/headers.
        guard let response = task.response as? HTTPURLResponse else {
            DiagnosticLogger.shared.log("NET-\(tag)", "didCompleteNoResponse", [
                "taskID": task.taskIdentifier,
            ])
            return
        }
        var fields: [String: Any] = [
            "taskID": task.taskIdentifier,
            "url": response.url?.absoluteString ?? "?",
            "status": response.statusCode,
        ]
        Self.collectInterestingHeaders(from: response, into: &fields)
        DiagnosticLogger.shared.log("NET-\(tag)", "didComplete", fields)
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void
    ) {
        var fields: [String: Any] = [
            "taskID": task.taskIdentifier,
            "fromStatus": response.statusCode,
            "fromURL": response.url?.absoluteString ?? "?",
            "toURL": request.url?.absoluteString ?? "?",
        ]
        Self.collectInterestingHeaders(from: response, into: &fields)
        DiagnosticLogger.shared.log("NET-\(tag)", "redirect", fields)
        completionHandler(request)
    }

    // MARK: - Helpers

    private func logRequest(_ event: String, request: URLRequest, taskID: Int) {
        DiagnosticLogger.shared.log("NET-\(tag)", event, [
            "taskID": taskID,
            "method": request.httpMethod ?? "?",
            "url": request.url?.absoluteString ?? "?",
            "ua": request.value(forHTTPHeaderField: "User-Agent") ?? "(default)",
            "referer": request.value(forHTTPHeaderField: "Referer") ?? "-",
            "origin": request.value(forHTTPHeaderField: "Origin") ?? "-",
            "cookieHeader": request.value(forHTTPHeaderField: "Cookie") ?? "-",
            "acceptLang": request.value(forHTTPHeaderField: "Accept-Language") ?? "-",
        ])
    }

    /// Pull just the CDN/edge headers that matter for diagnosing
    /// Turnstile / Cloudflare behaviour. Discards bulky / noisy
    /// headers (Content-Encoding, Date, Server-Timing big strings).
    private static func collectInterestingHeaders(
        from response: HTTPURLResponse,
        into fields: inout [String: Any]
    ) {
        let interesting = [
            "Set-Cookie", "CF-Ray", "CF-Cache-Status", "Server",
            "X-Vercel-Id", "X-Vercel-Cache",
            "X-Powered-By", "X-Robots-Tag",
            "Content-Type", "Content-Length",
            "X-Request-Id", "X-Cloudflare-Worker",
            "WWW-Authenticate", "PAT", // Private Access Token markers
        ]
        for name in interesting {
            // Header lookup is case-insensitive in HTTP/2 + responses,
            // so try the canonical and lower forms.
            if let v = response.value(forHTTPHeaderField: name)
                ?? response.allHeaderFields[name] as? String {
                // Trim to 200 chars so a 5 KB Set-Cookie doesn't take over a line.
                fields[name] = String(v.prefix(200))
            }
        }
    }
}
