#!/usr/bin/env bash

set -Eeuo pipefail

# ==============================================================================
# NextLimit - Génération du catalog.json global
#
# Usage :
#
#   ./generate-catalog.sh
#
# Arborescence attendue :
#
#   public-osm/data/
#   ├── packages/
#   │   ├── AD.json
#   │   ├── AD-2026.08.06.sqlite.gz
#   │   ├── AD-2026.08.06.sqlite.gz.sha256
#   │   ├── FR.json
#   │   ├── FR-2026.08.09.sqlite.gz
#   │   └── FR-2026.08.09.sqlite.gz.sha256
#   └── catalog.json
#
# Le script :
#
#   1. scanne packages/*.json
#   2. valide chaque manifest
#   3. vérifie package + checksum
#   4. valide SHA-256
#   5. vérifie les tailles
#   6. construit catalog.json v2
#   7. vérifie qu'aucune URL Geofabrik n'est publiée
#   8. publie catalog.json atomiquement
#
# ==============================================================================


SCRIPT_DIR="$(
    cd "$(dirname "${BASH_SOURCE[0]}")"
    pwd
)"


PUBLIC_ROOT="${PUBLIC_ROOT:-${SCRIPT_DIR}/public-osm/data}"

PACKAGES_DIR="${PACKAGES_DIR:-${PUBLIC_ROOT}/packages}"

CATALOG_FILE="${CATALOG_FILE:-${PUBLIC_ROOT}/catalog.json}"

CATALOG_TMP="${CATALOG_FILE}.tmp"

PYTHON_IMAGE="${PYTHON_IMAGE:-python:3.12-bookworm}"


# ==============================================================================
# Helpers
# ==============================================================================

log()
{
    printf \
        '\n\033[1;34m[NextLimit]\033[0m %s\n' \
        "$*"
}


success()
{
    printf \
        '\n\033[1;32m[NextLimit]\033[0m %s\n' \
        "$*"
}


warning()
{
    printf \
        '\n\033[1;33m[NextLimit]\033[0m %s\n' \
        "$*"
}


die()
{
    printf \
        '\n\033[1;31m[ERREUR]\033[0m %s\n' \
        "$*" >&2

    exit 1
}


# ==============================================================================
# Vérifications
# ==============================================================================

command -v docker >/dev/null 2>&1 \
    || die "Docker n'est pas installé."


docker info >/dev/null 2>&1 \
    || die "Docker n'est pas démarré ou inaccessible."


[[ -d "${PACKAGES_DIR}" ]] \
    || die "Répertoire packages introuvable : ${PACKAGES_DIR}"


mkdir -p \
    "${PUBLIC_ROOT}"


# ==============================================================================
# Construction catalogue
# ==============================================================================

log "Analyse des manifests dans : ${PACKAGES_DIR}"


docker run \
    --rm \
    --interactive \
    --user "$(id -u):$(id -g)" \
    --volume "${PUBLIC_ROOT}:/data" \
    "${PYTHON_IMAGE}" \
    python3 - <<'PY'

import datetime
import hashlib
import json
import os
import pathlib
import sys


ROOT = pathlib.Path("/data")
PACKAGES = ROOT / "packages"

CATALOG_TMP = ROOT / "catalog.json.tmp"


# ==============================================================================
# Helpers
# ==============================================================================

def fail(message):
    print(
        f"[ERREUR] {message}",
        file=sys.stderr,
    )
    raise SystemExit(1)


def sha256_file(path):
    digest = hashlib.sha256()

    with path.open("rb") as handle:
        while True:
            chunk = handle.read(1024 * 1024)

            if not chunk:
                break

            digest.update(chunk)

    return digest.hexdigest()


def parse_sha256_file(path):
    content = path.read_text(
        encoding="utf-8"
    ).strip()

    if not content:
        fail(
            f"Checksum vide : {path}"
        )

    parts = content.split()

    if len(parts) < 1:
        fail(
            f"Checksum invalide : {path}"
        )

    checksum = parts[0].strip().lower()

    if len(checksum) != 64:
        fail(
            f"SHA-256 invalide dans {path}"
        )

    return checksum


# ==============================================================================
# Scan manifests
# ==============================================================================

manifest_files = sorted(
    PACKAGES.glob("*.json")
)


if not manifest_files:
    fail(
        f"Aucun manifest trouvé dans {PACKAGES}"
    )


catalog_packages = []


for manifest_path in manifest_files:

    print(
        f"[NextLimit] Validation {manifest_path.name}"
    )


    try:
        manifest = json.loads(
            manifest_path.read_text(
                encoding="utf-8"
            )
        )
    except Exception as exc:
        fail(
            f"JSON invalide {manifest_path}: {exc}"
        )


    # ==========================================================================
    # Champs obligatoires
    # ==========================================================================

    required = [
        "id",
        "version",
        "format",
        "compression",
        "schemaVersion",
        "file",
        "checksumFile",
        "size",
        "installedSize",
        "generatedAt",
    ]


    for key in required:
        if key not in manifest:
            fail(
                f"{manifest_path.name}: champ manquant {key}"
            )


    package_id = str(
        manifest["id"]
    ).strip()


    if not package_id:
        fail(
            f"{manifest_path.name}: id vide"
        )


    if manifest["format"] != "nextlimit_sqlite":
        fail(
            f"{package_id}: format invalide "
            f"{manifest['format']!r}"
        )


    if manifest["compression"] != "gzip":
        fail(
            f"{package_id}: compression invalide "
            f"{manifest['compression']!r}"
        )


    if int(
        manifest["schemaVersion"]
    ) != 2:
        fail(
            f"{package_id}: schemaVersion != 2"
        )


    # ==========================================================================
    # Sécurité des noms de fichiers
    # ==========================================================================

    package_file_name = str(
        manifest["file"]
    )

    checksum_file_name = str(
        manifest["checksumFile"]
    )


    for value in (
        package_file_name,
        checksum_file_name,
    ):
        if (
            "/" in value
            or "\\" in value
            or ".." in value
            or "://" in value
        ):
            fail(
                f"{package_id}: nom de fichier non sûr : {value}"
            )


    if not package_file_name.endswith(
        ".sqlite.gz"
    ):
        fail(
            f"{package_id}: package non .sqlite.gz"
        )


    if checksum_file_name != (
        package_file_name + ".sha256"
    ):
        fail(
            f"{package_id}: checksumFile incohérent"
        )


    # ==========================================================================
    # Présence des fichiers
    # ==========================================================================

    package_path = (
        PACKAGES
        / package_file_name
    )

    checksum_path = (
        PACKAGES
        / checksum_file_name
    )


    if not package_path.is_file():
        fail(
            f"{package_id}: package absent : {package_path.name}"
        )


    if not checksum_path.is_file():
        fail(
            f"{package_id}: checksum absent : {checksum_path.name}"
        )


    # ==========================================================================
    # Taille package
    # ==========================================================================

    actual_size = package_path.stat().st_size

    expected_size = int(
        manifest["size"]
    )


    if actual_size != expected_size:
        fail(
            f"{package_id}: size incorrect "
            f"manifest={expected_size}, réel={actual_size}"
        )


    installed_size = int(
        manifest["installedSize"]
    )


    if installed_size <= 0:
        fail(
            f"{package_id}: installedSize invalide"
        )


    # ==========================================================================
    # SHA-256
    # ==========================================================================

    expected_sha = parse_sha256_file(
        checksum_path
    )

    actual_sha = sha256_file(
        package_path
    )


    if actual_sha != expected_sha:
        fail(
            f"{package_id}: SHA-256 invalide "
            f"{actual_sha} != {expected_sha}"
        )


    # ==========================================================================
    # Construction entrée catalogue
    # ==========================================================================

    entry = {
        "id": package_id,
        "name": manifest.get(
            "name",
            package_id,
        ),
        "version": manifest["version"],
        "format": "nextlimit_sqlite",
        "compression": "gzip",
        "schemaVersion": 2,
        "file": package_file_name,
        "checksumFile": checksum_file_name,
        "size": actual_size,
        "installedSize": installed_size,
        "generatedAt": manifest["generatedAt"],
    }


    # Champs optionnels

    if "iso3166_1" in manifest:
        entry["iso3166_1"] = manifest[
            "iso3166_1"
        ]

    if "geofabrikRegionId" in manifest:
        entry["geofabrikRegionId"] = manifest[
            "geofabrikRegionId"
        ]

    if manifest.get(
        "sourceTimestamp"
    ):
        entry["sourceTimestamp"] = manifest[
            "sourceTimestamp"
        ]

    if manifest.get(
        "segmentCount"
    ) is not None:
        entry["segmentCount"] = int(
            manifest[
                "segmentCount"
            ]
        )


    catalog_packages.append(
        entry
    )


# ==============================================================================
# Tri stable
# ==============================================================================

catalog_packages.sort(
    key=lambda package: package["id"]
)


# ==============================================================================
# Catalogue global
# ==============================================================================

catalog = {
    "version": 2,
    "region": "europe",
    "generatedAt": (
        datetime.datetime.now(
            datetime.timezone.utc
        )
        .replace(
            microsecond=0
        )
        .isoformat()
        .replace(
            "+00:00",
            "Z",
        )
    ),
    "count": len(
        catalog_packages
    ),
    "packages": catalog_packages,
}


serialized = json.dumps(
    catalog,
    ensure_ascii=False,
    indent=2,
)


# ==============================================================================
# Sécurité : aucune URL Geofabrik
# ==============================================================================

if "download.geofabrik.de" in serialized:
    fail(
        "Une URL Geofabrik est présente dans le catalogue public"
    )


# ==============================================================================
# Validation count
# ==============================================================================

if catalog["count"] != len(
    catalog["packages"]
):
    fail(
        "catalog.count incohérent"
    )


# ==============================================================================
# Écriture temporaire
# ==============================================================================

CATALOG_TMP.write_text(
    serialized + "\n",
    encoding="utf-8",
)


print()
print(
    f"[NextLimit] {catalog['count']} package(s) valide(s)"
)

print(
    f"[NextLimit] Catalogue temporaire : {CATALOG_TMP}"
)

PY


# ==============================================================================
# Vérification résultat
# ==============================================================================

[[ -s "${CATALOG_TMP}" ]] \
    || die "catalog.json.tmp n'a pas été créé."


# ==============================================================================
# Validation JSON finale
# ==============================================================================

docker run \
    --rm \
    --volume "${PUBLIC_ROOT}:/data:ro" \
    "${PYTHON_IMAGE}" \
    python3 - <<'PY'

import json
from pathlib import Path


path = Path(
    "/data/catalog.json.tmp"
)


with path.open(
    "r",
    encoding="utf-8",
) as handle:

    catalog = json.load(
        handle
    )


assert catalog["version"] == 2

assert catalog["region"] == "europe"

assert catalog["count"] == len(
    catalog["packages"]
)


for package in catalog[
    "packages"
]:

    assert (
        package["format"]
        == "nextlimit_sqlite"
    )

    assert (
        package["compression"]
        == "gzip"
    )

    assert (
        package["schemaVersion"]
        == 2
    )

    assert package["size"] > 0

    assert (
        package["installedSize"]
        > 0
    )


print(
    "Validation catalog.json : OK"
)

PY


# ==============================================================================
# Publication atomique
# ==============================================================================

mv \
    "${CATALOG_TMP}" \
    "${CATALOG_FILE}"


success \
    "catalog.json publié."


echo

echo \
    "Catalogue :"

echo \
    "  ${CATALOG_FILE}"

echo

echo \
    "Nombre de packages :"

docker run \
    --rm \
    --volume "${PUBLIC_ROOT}:/data:ro" \
    "${PYTHON_IMAGE}" \
    python3 -c \
    'import json; print(json.load(open("/data/catalog.json"))["count"])'

echo

echo \
    "Taille catalogue :"

du \
    -h \
    "${CATALOG_FILE}"

echo


