# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import argparse
import hashlib
import json
import logging
import re
from urllib.parse import urlparse, urlunparse

import requests


def fetch_url_for_cdms(cdms, urlParams):
    any_version = None
    for cdm in cdms:
        if "fileName" in cdm:
            cdm["fileUrl"] = cdm["fileName"].format_map(urlParams)
            response = requests.get(cdm["fileUrl"], allow_redirects=False)
            if response.status_code != 302:
                raise Exception(
                    "{} unexpected status code {}".format(
                        cdm["target"], response.status_code
                    )
                )

            redirectUrl = response.headers["Location"]
            parsedUrl = urlparse(redirectUrl)
            if parsedUrl.scheme != "https":
                raise Exception(
                    "{} expected https scheme '{}'".format(cdm["target"], redirectUrl)
                )

            sanitizedUrl = urlunparse(
                (parsedUrl.scheme, parsedUrl.netloc, parsedUrl.path, None, None, None)
            )

            # Note that here we modify the returned URL from the
            # component update service because it returns a preferred
            # server for the caller of the script. This may not match
            # up with what the end users require. Google has requested
            # that we instead replace these results with the
            # edgedl.me.gvt1.com domain/path, which should be location
            # agnostic.
            normalizedUrl = re.sub(
                r"https.+?release2",
                "https://edgedl.me.gvt1.com/edgedl/release2",
                sanitizedUrl,
            )
            if not normalizedUrl:
                raise Exception(
                    "{} cannot normalize '{}'".format(cdm["target"], sanitizedUrl)
                )

            # Because some users are unable to resolve *.gvt1.com
            # URLs, we supply an alternative based on www.google.com.
            # This should resolve with success more frequently.
            mirrorUrl = re.sub(
                r"https.+?release2",
                "https://www.google.com/dl/release2",
                sanitizedUrl,
            )

            version = re.search(r".*?_([\d]+\.[\d]+\.[\d]+\.[\d]+)/", sanitizedUrl)
            if version is None:
                raise Exception(
                    "{} cannot extract version '{}'".format(cdm["target"], sanitizedUrl)
                )
            if any_version is None:
                any_version = version.group(1)
            elif version.group(1) != any_version:
                raise Exception(
                    "{} version {} mismatch {}".format(
                        cdm["target"], version.group(1), any_version
                    )
                )
            cdm["fileName"] = normalizedUrl
            if mirrorUrl and mirrorUrl != normalizedUrl:
                cdm["fileNameMirror"] = mirrorUrl
    return any_version


def fetch_data_for_cdms(cdms, urlParams):
    for cdm in cdms:
        if "fileName" in cdm:
            cdm["fileUrl"] = cdm["fileName"].format_map(urlParams)
            response = requests.get(cdm["fileUrl"])
            response.raise_for_status()
            cdm["hashValue"] = hashlib.sha512(response.content).hexdigest()
            if "fileNameMirror" in cdm:
                cdm["mirrorUrl"] = cdm["fileNameMirror"].format_map(urlParams)
                mirrorresponse = requests.get(cdm["mirrorUrl"])
                mirrorresponse.raise_for_status()
                mirrorhash = hashlib.sha512(mirrorresponse.content).hexdigest()
                if cdm["hashValue"] != mirrorhash:
                    raise Exception(
                        "Primary hash {} and mirror hash {} differ",
                        cdm["hashValue"],
                        mirrorhash,
                    )
            cdm["filesize"] = len(response.content)
            if cdm["filesize"] == 0:
                raise Exception("Empty response for {target}".format_map(cdm))


def generate_json_for_cdms(cdms):
    cdm_json = ""
    for cdm in cdms:
        if "alias" in cdm:
            cdm_json += (
                '        "{target}": {{\n'
                + '          "alias": "{alias}"\n'
                + "        }},\n"
            ).format_map(cdm)
        elif "mirrorUrl" in cdm:
            cdm_json += (
                '        "{target}": {{\n'
                + '          "fileUrl": "{fileUrl}",\n'
                + '          "mirrorUrls": [\n'
                + '            "{mirrorUrl}"\n'
                + "          ],\n"
                + '          "filesize": {filesize},\n'
                + '          "hashValue": "{hashValue}"\n'
                + "        }},\n"
            ).format_map(cdm)
        else:
            cdm_json += (
                '        "{target}": {{\n'
                + '          "fileUrl": "{fileUrl}",\n'
                + '          "mirrorUrls": [],\n'
                + '          "filesize": {filesize},\n'
                + '          "hashValue": "{hashValue}"\n'
                + "        }},\n"
            ).format_map(cdm)
    return cdm_json[:-2] + "\n"


def calculate_gmpopenh264_json(version: str, version_hash: str, url_base: str) -> str:
    # fmt: off
    cdms = [
        {"target": "Darwin_aarch64-gcc3", "fileName": "{url_base}/openh264-macosx64-aarch64-{version}.zip"},
        {"target": "Darwin_x86_64-gcc3", "fileName": "{url_base}/openh264-macosx64-{version}.zip"},
        {"target": "Linux_aarch64-gcc3", "fileName": "{url_base}/openh264-linux64-aarch64-{version}.zip"},
        {"target": "Linux_x86-gcc3", "fileName": "{url_base}/openh264-linux32-{version}.zip"},
        {"target": "Linux_x86_64-gcc3", "fileName": "{url_base}/openh264-linux64-{version}.zip"},
        {"target": "Linux_x86_64-gcc3-asan", "alias": "Linux_x86_64-gcc3"},
        {"target": "WINNT_aarch64-msvc-aarch64", "fileName": "{url_base}/openh264-win64-aarch64-{version}.zip"},
        {"target": "WINNT_x86-msvc", "fileName": "{url_base}/openh264-win32-{version}.zip"},
        {"target": "WINNT_x86-msvc-x64", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86-msvc-x86", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}/openh264-win64-{version}.zip"},
        {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
        {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
    ]
    # fmt: on
    try:
        fetch_data_for_cdms(cdms, {"url_base": url_base, "version": version_hash})
    except Exception as e:
        logging.error("calculate_gmpopenh264_json: could not create JSON due to: %s", e)
        return ""
    else:
        return (
            "{\n"
            + '  "hashFunction": "sha512",\n'
            + f'  "name": "OpenH264-{version}",\n'
            + '  "schema_version": 1000,\n'
            + '  "vendors": {\n'
            + '    "gmp-gmpopenh264": {\n'
            + '      "platforms": {\n'
            + generate_json_for_cdms(cdms)
            + "      },\n"
            + f'      "version": "{version}"\n'
            + "    }\n"
            + "  }\n"
            + "}"
        )


def calculate_widevinecdm_json(version: str, url_base: str) -> str:
    # fmt: off
    cdms = [
        {"target": "Darwin_aarch64-gcc3", "fileName": "{url_base}/{version}-mac-arm64.zip"},
        {"target": "Darwin_x86_64-gcc3", "alias": "Darwin_x86_64-gcc3-u-i386-x86_64"},
        {"target": "Darwin_x86_64-gcc3-u-i386-x86_64", "fileName": "{url_base}/{version}-mac-x64.zip"},
        {"target": "Linux_x86_64-gcc3", "fileName": "{url_base}/{version}-linux-x64.zip"},
        {"target": "Linux_x86_64-gcc3-asan", "alias": "Linux_x86_64-gcc3"},
        {"target": "WINNT_aarch64-msvc-aarch64", "fileName": "{url_base}/{version}-win-arm64.zip"},
        {"target": "WINNT_x86-msvc", "fileName": "{url_base}/{version}-win-x86.zip"},
        {"target": "WINNT_x86-msvc-x64", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86-msvc-x86", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}/{version}-win-x64.zip"},
        {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
        {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
    ]
    # fmt: on
    try:
        fetch_data_for_cdms(cdms, {"url_base": url_base, "version": version})
    except Exception as e:
        logging.error("calculate_widevinecdm_json: could not create JSON due to: %s", e)
        return ""
    else:
        return (
            "{\n"
            + '  "hashFunction": "sha512",\n'
            + f'  "name": "Widevine-{version}",\n'
            + '  "schema_version": 1000,\n'
            + '  "vendors": {\n'
            + '    "gmp-widevinecdm": {\n'
            + '      "platforms": {\n'
            + generate_json_for_cdms(cdms)
            + "      },\n"
            + f'      "version": "{version}"\n'
            + "    }\n"
            + "  }\n"
            + "}"
        )


def calculate_chrome_component_json(
    name: str, altname: str, url_base: str, cdms
) -> str:
    try:
        version = fetch_url_for_cdms(cdms, {"url_base": url_base})
        fetch_data_for_cdms(cdms, {})
    except Exception as e:
        logging.error(
            "calculate_chrome_component_json: could not create JSON due to: %s", e
        )
        return ""
    else:
        return (
            "{\n"
            + '  "hashFunction": "sha512",\n'
            + f'  "name": "{name}-{version}",\n'
            + '  "schema_version": 1000,\n'
            + '  "vendors": {\n'
            + f'    "gmp-{altname}": {{\n'
            + '      "platforms": {\n'
            + generate_json_for_cdms(cdms)
            + "      },\n"
            + f'      "version": "{version}"\n'
            + "    }\n"
            + "  }\n"
            + "}"
        )


def calculate_widevinecdm_component_json(url_base: str) -> str:
    # fmt: off
    cdms = [
        {"target": "Darwin_aarch64-gcc3", "fileName": "{url_base}&os=mac&arch=arm64&os_arch=arm64"},
        {"target": "Darwin_x86_64-gcc3", "alias": "Darwin_x86_64-gcc3-u-i386-x86_64"},
        {"target": "Darwin_x86_64-gcc3-u-i386-x86_64", "fileName": "{url_base}&os=mac&arch=x64&os_arch=x64"},
        {"target": "Linux_x86_64-gcc3", "fileName": "{url_base}&os=Linux&arch=x64&os_arch=x64"},
        {"target": "Linux_x86_64-gcc3-asan", "alias": "Linux_x86_64-gcc3"},
        {"target": "WINNT_aarch64-msvc-aarch64", "fileName": "{url_base}&os=win&arch=arm64&os_arch=arm64"},
        {"target": "WINNT_x86-msvc", "fileName": "{url_base}&os=win&arch=x86&os_arch=x86"},
        {"target": "WINNT_x86-msvc-x64", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86-msvc-x86", "alias": "WINNT_x86-msvc"},
        {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}&os=win&arch=x64&os_arch=x64"},
        {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
        {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
    ]
    # fmt: on
    return calculate_chrome_component_json(
        "Widevine",
        "widevinecdm",
        url_base.format_map({"guid": "oimompecagnajdejgnnjijobebaeigek"}),
        cdms,
    )


def calculate_widevinecdm_l1_component_json(url_base: str) -> str:
    # fmt: off
    cdms = [
        {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}&os=win&arch=x64&os_arch=x64"},
        {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
        {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
    ]
    # fmt: on
    return calculate_chrome_component_json(
        "Widevine-L1",
        "widevinecdm-l1",
        url_base.format_map({"guid": "neifaoindggfcjicffkgpmnlppeffabd"}),
        cdms,
    )


def main():
    examples = """examples:
  python dom/media/tools/generateGmpJson.py widevine 4.10.2557.0 >toolkit/content/gmp-sources/widevinecdm.json
  python dom/media/tools/generateGmpJson.py --url http://localhost:8080 openh264 2.3.1 0a48f4d2e9be2abb4fb01b4c3be83cf44ce91a6e
  python dom/media/tools/generateGmpJson.py widevine_component"""

    parser = argparse.ArgumentParser(
        description="Generate JSON for GMP plugin updates",
        epilog=examples,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "plugin",
        help="which plugin: openh264, widevine, widevine_component, widevine_l1_component",
    )
    parser.add_argument("version", help="version of plugin", nargs="?")
    parser.add_argument("revision", help="revision hash of plugin", nargs="?")
    parser.add_argument("--url", help="override base URL from which to fetch plugins")
    parser.add_argument(
        "--testrequest",
        action="store_true",
        help="request upcoming version for component update service",
    )
    args = parser.parse_args()

    if args.plugin == "openh264":
        url_base = "https://ciscobinary.openh264.org"
        if args.version is None or args.revision is None:
            parser.error("openh264 requires version and revision")
    elif args.plugin == "widevine":
        url_base = "https://redirector.gvt1.com/edgedl/widevine-cdm"
        if args.version is None:
            parser.error("widevine requires version")
        if args.revision is not None:
            parser.error("widevine cannot use revision")
    elif args.plugin in ("widevine_component", "widevine_l1_component"):
        url_base = "https://update.googleapis.com/service/update2/crx?response=redirect&x=id%3D{guid}%26uc&acceptformat=crx3&updaterversion=999"
        if args.testrequest:
            url_base += "&testrequest=1"
        if args.version is not None or args.revision is not None:
            parser.error("chrome component cannot use version or revision")
    else:
        parser.error("plugin not recognized")

    if args.url is not None:
        url_base = args.url

    if url_base[-1] == "/":
        url_base = url_base[:-1]

    if args.plugin == "openh264":
        json_result = calculate_gmpopenh264_json(args.version, args.revision, url_base)
    elif args.plugin == "widevine":
        json_result = calculate_widevinecdm_json(args.version, url_base)
    elif args.plugin == "widevine_component":
        json_result = calculate_widevinecdm_component_json(url_base)
    elif args.plugin == "widevine_l1_component":
        json_result = calculate_widevinecdm_l1_component_json(url_base)

    try:
        json.loads(json_result)
    except json.JSONDecodeError as e:
        logging.error("invalid JSON produced: %s", e)
    else:
        print(json_result)


main()
