#!/usr/bin/env python3

# Copyright (c) 2025, The Ladybird contributors
#
# SPDX-License-Identifier: BSD-2-Clause

import glob
import json
import os
import re
import subprocess
import sys
import urllib.request

from enum import Enum

VCPKG = "vcpkg.json"
VCPKG_OVERLAYS_PORTS = "Meta/CMake/vcpkg/overlay-ports/*"
VCPKG_REPO = "Build/vcpkg"
VCPKG_BASELINE_URL = "https://raw.githubusercontent.com/microsoft/vcpkg/{}/versions/baseline.json"
FLATPAK_MANIFEST = "Meta/CMake/flatpak/org.ladybird.Ladybird.json"
SELF = "Ladybird"

# List of build tools that are not provided by the Flatpak SDK and therefore in the manifest
# For a vcpkg build these are installed on the host system
flatpak_build_tools = [
    "gn",
]

# List of libraries that are transitive dependencies
# These are in the manifest to fetch before building, but not explicitely versioned
flatpak_transitive_deps = [
    "libyuv",
]

# List of libraries that are in the Flatpak runtime and therefore not in the manifest
# See: https://docs.flatpak.org/en/latest/available-runtimes.html#check-software-available-in-runtimes
flatpak_runtime_libs = [
    "curl",
    "dbus",
    "fontconfig",
    "freetype",
    "harfbuzz",
    "libjpeg-turbo",
    "libproxy",
    "nghttp2",  # FIXME: This can be removed when the vcpkg overlay is no longer needed.
    "qtbase",
    "qtmultimedia",
    "sqlite3",
    "tiff",
    "vulkan",
    "vulkan-headers",
    "zlib",
]

# List of libraries that are in vcpkg but not needed for the Linux platform
vcpkg_not_linux = [
    "dirent",
    "mman",
    "pthread",
]

# List of libraries that are behind vcpkg manifest features and only
# installed for specific GUI frameworks (e.g. GTK), not the Qt flatpak.
vcpkg_feature_only = [
    "gtk",
    "libadwaita",
    "wayland",
    "wayland-protocols",
]


class DepMatch(Enum):
    Match = (0,)
    NoMatch = (1,)
    VersionMismatch = (2,)
    Excluded = (3,)


baseline_versions = {}


def get_baseline_version(baseline, name):
    if baseline not in baseline_versions:
        if os.path.isdir(VCPKG_REPO):
            # Clear GIT_DIR so git operates on the vcpkg repo, not the parent repo (pre-commit sets GIT_DIR)
            env = {k: v for k, v in os.environ.items() if k != "GIT_DIR"}
            try:
                result = subprocess.run(
                    ["git", "-C", VCPKG_REPO, "show", f"{baseline}:versions/baseline.json"],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.DEVNULL,
                    check=True,
                    env=env,
                    text=True,
                )
                baseline_versions[baseline] = json.loads(result.stdout)
            except (subprocess.CalledProcessError, json.JSONDecodeError):
                pass

        if baseline not in baseline_versions:
            url = VCPKG_BASELINE_URL.format(baseline)

            try:
                with urllib.request.urlopen(url) as response:
                    baseline_versions[baseline] = json.load(response)
            except Exception as error:
                print(f"Failed to fetch vcpkg baseline {baseline[:7]}: {error}")
                baseline_versions[baseline] = {}

    port = baseline_versions[baseline].get("default", {}).get(name)
    if port and "baseline" in port:
        return port["baseline"]

    print(f"{name} cannot be matched, vcpkg baseline revision: {baseline[:7]}")


def check_for_match(vcpkg: dict, vcpkg_baseline, name: str, identifier: str) -> DepMatch:
    if name == SELF or name in flatpak_build_tools or name in flatpak_transitive_deps:
        return DepMatch.Excluded

    if name not in vcpkg:
        version = get_baseline_version(vcpkg_baseline, name)

        if version:
            vcpkg[name] = version

    if name in vcpkg:
        version = vcpkg[name]
        normalized_version = re.sub(r"[^A-Za-z0-9]", "", version)
        normalized_identifier = re.sub(r"[^A-Za-z0-9]", "", identifier)
        if version not in identifier and normalized_version not in normalized_identifier:
            return DepMatch.VersionMismatch
        else:
            return DepMatch.Match
    else:
        return DepMatch.NoMatch


def check_vcpkg_vs_flatpak_versioning():
    def match_and_update(name: str, identifier: str) -> bool:
        dep_match = check_for_match(vcpkg, vcpkg_baseline, name, identifier)

        if dep_match == DepMatch.Match or dep_match == DepMatch.Excluded:
            if name in vcpkg:
                del vcpkg[name]

            return False
        else:
            if dep_match == DepMatch.VersionMismatch:
                print(f"{name} version mismatch. vcpkg: {vcpkg[name]}, Flatpak: {source['tag']}")
                del vcpkg[name]
            elif dep_match == DepMatch.NoMatch:
                flatpak.append(name)

            return True

    vcpkg = {}
    flatpak = []
    mismatch_found = False

    with open(VCPKG) as input:
        vcpkg_json = json.load(input)
        vcpkg_baseline = vcpkg_json["builtin-baseline"]

        for package in vcpkg_json["overrides"]:
            # Add name/version pair, strip any '#' postfix from the version
            vcpkg[package["name"]] = str(package["version"]).split("#")[0]

    # Check the vcpkg overlay ports for packages not listed in overrides
    for path in glob.glob(VCPKG_OVERLAYS_PORTS):
        with open(f"{path}/vcpkg.json") as input:
            overlay = json.load(input)

            if "name" in overlay and overlay["name"] not in vcpkg and "version" in overlay:
                vcpkg[overlay["name"]] = overlay["version"]

    with open(FLATPAK_MANIFEST) as input:
        for module in json.load(input)["modules"]:
            name = module["name"]

            for source in module["sources"]:
                if "type" in source and source["type"] == "git":
                    if "tag" in source:
                        # Get the tag
                        # Replace '-' with '.': 76-1 vs 76.1
                        tag = str(source["tag"]).replace("-", ".")
                        mismatch_found |= match_and_update(name, tag)

                        break
                    elif "branch" in source or "x-branch" in source:
                        # Get the branch
                        # Strip '_' postfix, replace '/' with '_': chromium/7258_13 vs chromium_7258
                        branch = str(source.get("branch", source.get("x-branch"))).split("_")[0].replace("/", "_")
                        mismatch_found |= match_and_update(name, branch)

                        break
                elif "type" in source and source["type"] == "archive":
                    mismatch_found |= match_and_update(name, source["url"])

                    break
            else:
                if not (name == SELF or name in flatpak_build_tools or name in flatpak_transitive_deps):
                    flatpak.append(name)

        # Remove excluded dependencies from the vcpkg list
        for name in flatpak_runtime_libs + vcpkg_not_linux + vcpkg_feature_only:
            if name in vcpkg:
                del vcpkg[name]

        if len(vcpkg):
            mismatch_found = True
            print(f"There are {len(vcpkg)} vcpkg dependencies that have no match in flatpak")

            for name in vcpkg.keys():
                print(f"- {name}")

        if len(flatpak):
            mismatch_found = True
            print(f"There are {len(flatpak)} flatpak dependencies that have no match in vcpkg")

            for name in flatpak:
                print(f"- {name}")

    return mismatch_found


def main():
    file_list = sys.argv[1:] if len(sys.argv) > 1 else [VCPKG]
    did_fail = False

    if VCPKG in file_list or FLATPAK_MANIFEST in file_list:
        did_fail = check_vcpkg_vs_flatpak_versioning()

    # TODO: Add linting of Flatpak and AppStream manifest
    # See #5594, bullet point "Missing lint job in CI to ensure Flatpak and AppStream manifests are up to snuff"

    if did_fail:
        sys.exit(1)


if __name__ == "__main__":
    main()
