/*
 * Copyright (C) 2017-2026 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

// HTTP Archive (HAR) format - Version 1.2
// https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html#sec-har-object-types-creator
// http://www.softwareishard.com/blog/har-12-spec/

WI.HARBuilder = class HARBuilder
{
    static async buildArchive(entries)
    {
        let promises = [];
        for (let entry of entries) {
            // Handle redirect entries (which don't need content)
            if (entry.redirect) {
                promises.push(Promise.resolve());
                continue;
            }

            // Handle regular resources
            console.assert(entry.finished);
            promises.push(new Promise((resolve, reject) => {
                // Always resolve.
                entry.requestContent().then(
                    (x) => resolve(x),
                    () => resolve(null)
                );
            }));
        }

        let contents = await Promise.all(promises);
        console.assert(contents.length === entries.length);

        return {
            log: {
                version: "1.2",
                creator: HARBuilder.creator(),
                pages: HARBuilder.pages(),
                entries: entries.map((entry, index) => HARBuilder.entry(entry, contents[index])),
            }
        };
    }

    static creator()
    {
        return {
            name: "WebKit Web Inspector",
            version: "1.0",
        };
    }

    static pages()
    {
        return [{
            startedDateTime: HARBuilder.date(WI.networkManager.mainFrame.mainResource.requestSentDate),
            id: "page_0",
            title: WI.networkManager.mainFrame.url || "",
            pageTimings: HARBuilder.pageTimings(),
        }];
    }

    static pageTimings()
    {
        let result = {};

        let domContentReadyEventTimestamp = WI.networkManager.mainFrame.domContentReadyEventTimestamp;
        if (!isNaN(domContentReadyEventTimestamp))
            result.onContentLoad = domContentReadyEventTimestamp * 1000;

        let loadEventTimestamp = WI.networkManager.mainFrame.loadEventTimestamp;
        if (!isNaN(loadEventTimestamp))
            result.onLoad = loadEventTimestamp * 1000;

        return result;
    }

    static entry(entryOrResource, content)
    {
        // Handle redirect entries
        if (entryOrResource.redirect) {
            let redirect = entryOrResource.redirect;
            let parentResource = entryOrResource.parentResource;

            return {
                pageref: "page_0",
                startedDateTime: HARBuilder.date(new Date(redirect.timestamp * 1000)),
                time: 0, // Redirects don't have detailed timing
                request: HARBuilder.requestForRedirect(redirect, parentResource),
                response: HARBuilder.responseForRedirect(redirect),
                cache: {},
                timings: HARBuilder.timingsForRedirect(),
                connection: String(parentResource.connectionIdentifier),
                serverIPAddress: parentResource.remoteAddress ? HARBuilder.ipAddress(parentResource.remoteAddress) : undefined,
                _serverPort: parentResource.remoteAddress ? HARBuilder.port(parentResource.remoteAddress) : undefined,
            };
        }

        // Handle regular resources
        let resource = entryOrResource;
        let entry = {
            pageref: "page_0",
            startedDateTime: HARBuilder.date(resource.requestSentDate),
            time: 0,
            request: HARBuilder.request(resource),
            response: HARBuilder.response(resource, content),
            cache: HARBuilder.cache(resource),
            timings: HARBuilder.timings(resource),
        };

        if (resource.timingData.startTime && resource.timingData.responseEnd)
            entry.time = (resource.timingData.responseEnd - resource.timingData.startTime) * 1000;
        if (resource.remoteAddress) {
            entry.serverIPAddress = HARBuilder.ipAddress(resource.remoteAddress);

            // WebKit Custom Field `_serverPort`.
            if (entry.serverIPAddress)
                entry._serverPort = HARBuilder.port(resource.remoteAddress);
        }
        if (resource.connectionIdentifier)
            entry.connection = String(resource.connectionIdentifier);

        // CFNetwork Custom Field `_fetchType`.
        if (resource.responseSource !== WI.Resource.ResponseSource.Unknown)
            entry._fetchType = HARBuilder.fetchType(resource.responseSource);

        // WebKit Custom Field `_priority`.
        if (resource.priority !== WI.Resource.NetworkPriority.Unknown)
            entry._priority = HARBuilder.priority(resource.priority);

        return entry;
    }

    static request(resource)
    {
        let result = {
            method: resource.requestMethod || "",
            url: resource.url || "",
            httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
            cookies: HARBuilder.cookies(resource.requestCookies, null),
            headers: HARBuilder.headers(resource.requestHeaders),
            queryString: resource.queryStringParameters || [],
            headersSize: !isNaN(resource.requestHeadersTransferSize) ? resource.requestHeadersTransferSize : -1,
            bodySize: !isNaN(resource.requestBodyTransferSize) ? resource.requestBodyTransferSize : -1,
        };

        if (resource.requestData)
            result.postData = HARBuilder.postData(resource);

        return result;
    }

    static response(resource, content)
    {
        let result = {
            status: resource.statusCode || 0,
            statusText: resource.statusText || "",
            httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
            cookies: HARBuilder.cookies(resource.responseCookies, resource.requestSentDate),
            headers: HARBuilder.headers(resource.responseHeaders),
            content: HARBuilder.content(resource, content),
            redirectURL: resource.responseHeaders.valueForCaseInsensitiveKey("Location") || "",
            headersSize: !isNaN(resource.responseHeadersTransferSize) ? resource.responseHeadersTransferSize : -1,
            bodySize: !isNaN(resource.responseBodyTransferSize) ? resource.responseBodyTransferSize : -1,
        };

        // Chrome Custom Field `_transferSize`.
        if (!isNaN(resource.networkTotalTransferSize))
            result._transferSize = resource.networkTotalTransferSize;

        // Chrome Custom Field `_error`.
        if (resource.failureReasonText)
            result._error = resource.failureReasonText;

        return result;
    }

    static cookies(cookies, requestSentDate)
    {
        let result = [];

        for (let cookie of cookies) {
            let json = {
                name: cookie.name,
                value: cookie.value,
            };

            if (cookie.type === WI.Cookie.Type.Response) {
                if (cookie.path)
                    json.path = cookie.path;
                if (cookie.domain)
                    json.domain = cookie.domain;
                json.expires = HARBuilder.date(cookie.expirationDate(requestSentDate));
                json.httpOnly = cookie.httpOnly;
                json.secure = cookie.secure;
                if (cookie.sameSite !== WI.Cookie.SameSiteType.None)
                    json.sameSite = cookie.sameSite;
            }

            result.push(json);
        }

        return result;
    }

    static headers(headers)
    {
        let result = [];

        for (let key in headers)
            result.push({name: key, value: headers[key]});

        return result;
    }

    static content(resource, content)
    {
        let encodedSize = !isNaN(resource.networkEncodedSize) ? resource.networkEncodedSize : resource.estimatedNetworkEncodedSize;
        let decodedSize = !isNaN(resource.networkDecodedSize) ? resource.networkDecodedSize : resource.size;

        if (isNaN(decodedSize))
            decodedSize = 0;
        if (isNaN(encodedSize))
            encodedSize = 0;

        let result = {
            size: decodedSize,
            compression: decodedSize - encodedSize,
            mimeType: resource.mimeType || "x-unknown",
        };

        if (content) {
            if (content.rawContent)
                result.text = content.rawContent;
            if (content.rawBase64Encoded)
                result.encoding = "base64";
        }

        return result;
    }

    static postData(resource)
    {
        return {
            mimeType: resource.requestDataContentType || "",
            text: resource.requestData,
            params: resource.requestFormParameters || [],
        };
    }

    static cache(resource)
    {
        // FIXME: <https://webkit.org/b/178682> Web Inspector: Include <cache> details in HAR Export
        // http://www.softwareishard.com/blog/har-12-spec/#cache
        return {};
    }

    static timings(resource)
    {
        // FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info
        // Chrome has Custom Fields `_blocked_queueing` and `_blocked_proxy`.

        let result = {
            blocked: -1,
            dns: -1,
            connect: -1,
            ssl: -1,
            send: 0,
            wait: 0,
            receive: 0,
        };

        if (resource.timingData.startTime && resource.timingData.responseEnd) {
            let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
            result.blocked = ((domainLookupStart || connectStart || requestStart) - startTime) * 1000;
            if (domainLookupStart)
                result.dns = ((domainLookupEnd || connectStart || requestStart) - domainLookupStart) * 1000;
            if (connectStart)
                result.connect = ((connectEnd || requestStart) - connectStart) * 1000;
            if (secureConnectionStart)
                result.ssl = ((connectEnd || requestStart) - secureConnectionStart) * 1000;

            // If all the time before requestStart was included in blocked, then make send time zero
            // as send time is essentially just blocked time after dns / connection time, and we
            // do not want to double count it.
            result.send = (domainLookupEnd || connectEnd) ? (requestStart - (connectEnd || domainLookupEnd)) * 1000 : 0;

            result.wait = (responseStart - requestStart) * 1000;
            result.receive = (responseEnd - responseStart) * 1000;
        }

        return result;
    }

    static requestForRedirect(redirect, parentResource)
    {
        return {
            method: redirect.requestMethod || "",
            url: redirect.url || "",
            httpVersion: "", // Not available for redirects
            cookies: [], // Not available for redirects
            headers: HARBuilder.headers(redirect.requestHeaders),
            queryString: [], // Could parse from URL if needed
            headersSize: -1,
            bodySize: -1,
        };
    }

    static responseForRedirect(redirect)
    {
        return {
            status: redirect.responseStatusCode || 0,
            statusText: redirect.responseStatusText || "",
            httpVersion: "", // Not available for redirects
            cookies: [], // Not available for redirects
            headers: HARBuilder.headers(redirect.responseHeaders),
            content: {
                size: 0,
                mimeType: "x-unknown",
            },
            redirectURL: redirect.responseHeaders.Location || redirect.responseHeaders.location || "",
            headersSize: -1,
            bodySize: -1,
        };
    }

    static timingsForRedirect()
    {
        // FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info
        // Redirects don't currently have detailed timing breakdown
        return {
            blocked: -1,
            dns: -1,
            connect: -1,
            ssl: -1,
            send: 0,
            wait: 0,
            receive: 0,
        };
    }

    // Helpers

    static ipAddress(remoteAddress)
    {
        // IP Address, without port.
        if (!remoteAddress)
            return "";

        // NOTE: Resource.remoteAddress always includes the port at the end.
        // So this always strips the last part.
        return remoteAddress.replace(/:\d+$/, "");
    }

    static port(remoteAddress)
    {
        // IP Address, without port.
        if (!remoteAddress)
            return undefined;

        // NOTE: Resource.remoteAddress always includes the port at the end.
        // So this always matches the last part.
        let index = remoteAddress.lastIndexOf(":");
        if (!index)
            return undefined;

        let portString = remoteAddress.substring(index + 1);
        let port = parseInt(portString);
        if (isNaN(port))
            return undefined;

        return port;
    }

    static date(date)
    {
        // ISO 8601
        if (!date)
            return "";

        return date.toISOString();
    }

    static fetchType(responseSource)
    {
        switch (responseSource) {
        case WI.Resource.ResponseSource.Network:
            return "Network Load";
        case WI.Resource.ResponseSource.MemoryCache:
            return "Memory Cache";
        case WI.Resource.ResponseSource.DiskCache:
            return "Disk Cache";
        case WI.Resource.ResponseSource.ServiceWorker:
            return "Service Worker";
        case WI.Resource.ResponseSource.InspectorOverride:
            return "Inspector Override";
        }

        console.assert();
        return undefined;
    }

    static priority(priority)
    {
        switch (priority) {
        case WI.Resource.NetworkPriority.Low:
            return "low";
        case WI.Resource.NetworkPriority.Medium:
            return "medium";
        case WI.Resource.NetworkPriority.High:
            return "high";
        }

        console.assert();
        return undefined;
    }

    // Consuming.

    static dateFromHARDate(isoString)
    {
        return Date.parse(isoString);
    }

    static protocolFromHARProtocol(protocol)
    {
        switch (protocol) {
        case "HTTP/2":
            return "h2";
        case "HTTP/1.0":
            return "http/1.0";
        case "HTTP/1.1":
            return "http/1.1";
        case "SPDY/2":
            return "spdy/2";
        case "SPDY/3":
            return "spdy/3";
        case "SPDY/3.1":
            return "spdy/3.1";
        }

        if (protocol)
            console.warn("Unknown HAR protocol value", protocol);
        return null;
    }

    static responseSourceFromHARFetchType(fetchType)
    {
        switch (fetchType) {
        case "Network Load":
            return WI.Resource.ResponseSource.Network;
        case "Memory Cache":
            return WI.Resource.ResponseSource.MemoryCache;
        case "Disk Cache":
            return WI.Resource.ResponseSource.DiskCache;
        case "Service Worker":
            return WI.Resource.ResponseSource.ServiceWorker;
        case "Inspector Override":
            return WI.Resource.ResponseSource.InspectorOverride;
        }

        if (fetchType)
            console.warn("Unknown HAR _fetchType value", fetchType);
        return WI.Resource.ResponseSource.Other;
    }

    static networkPriorityFromHARPriority(priority)
    {
        switch (priority) {
        case "low":
            return WI.Resource.NetworkPriority.Low;
        case "medium":
            return WI.Resource.NetworkPriority.Medium;
        case "high":
            return WI.Resource.NetworkPriority.High;
        }

        if (priority)
            console.warn("Unknown HAR priority value", priority);
        return WI.Resource.NetworkPriority.Unknown;
    }
};
