#!/usr/bin/env python3
"""Check the basic structure of one Extension before packaging it."""

import json
import re
import sys
from pathlib import Path, PurePosixPath
from urllib.parse import urlsplit


ROOT = Path(__file__).resolve().parents[1]
ID_RE = re.compile(r"[a-z][a-z0-9._-]*\Z")


def fail(message: str) -> None:
    raise SystemExit(f"Extension check failed: {message}")


def file_at(source: Path, value: object, suffixes: tuple[str, ...]) -> None:
    if not isinstance(value, str) or not value or "\\" in value:
        fail(f"invalid file path: {value!r}")
    path = PurePosixPath(value)
    if path.is_absolute() or value != path.as_posix() or any(part in (".", "..") for part in path.parts):
        fail(f"file path must stay inside the Extension: {value}")
    if not value.endswith(suffixes):
        fail(f"unexpected file type: {value}")
    target = source.joinpath(*path.parts)
    if target.is_symlink() or not target.is_file():
        fail(f"missing regular file: {value}")


def main() -> None:
    if len(sys.argv) != 2 or not ID_RE.fullmatch(sys.argv[1]):
        fail("usage: bin/check EXTENSION_ID")
    extension_id = sys.argv[1]
    source = ROOT / "extensions" / extension_id
    if not source.is_dir() or source.is_symlink():
        fail(f"missing Extension folder: {source}")
    for path in source.rglob("*"):
        relative = path.relative_to(source)
        if any(part.startswith(".") for part in relative.parts) or "node_modules" in relative.parts:
            fail(f"remove hidden files and dependency folders before packaging: {relative}")
        if path.is_symlink():
            fail(f"symlinks are not allowed: {relative}")
        if not path.is_file() and not path.is_dir():
            fail(f"unsupported entry: {relative}")
    try:
        manifest = json.loads((source / "manifest.json").read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        fail(f"cannot read manifest.json: {error}")
    if not isinstance(manifest, dict):
        fail("manifest.json must contain an object")
    if manifest.get("schema") != "ele.extension.manifest/v1":
        fail("manifest schema must be ele.extension.manifest/v1")
    if manifest.get("id") != extension_id:
        fail("manifest id must match the Extension folder")
    state_schema = manifest.get("state_schema", 1)
    if isinstance(state_schema, bool) or not isinstance(state_schema, int) or not 0 <= state_schema <= 1000:
        fail("state_schema must be an integer from 0 through 1000")
    for field in ("name", "version"):
        if not isinstance(manifest.get(field), str) or not manifest[field].strip():
            fail(f"manifest {field} must be nonempty text")
    file_at(source, manifest.get("main"), (".js", ".mjs"))
    styles = manifest.get("styles", [])
    if not isinstance(styles, list):
        fail("manifest styles must be a list")
    for style in styles:
        file_at(source, style, (".css",))
    views = manifest.get("views", [])
    if not isinstance(views, list) or len(views) > 64:
        fail("manifest views must be a list of at most 64 entries")
    seen_views = set()
    for view in views:
        if not isinstance(view, dict) or not isinstance(view.get("id"), str) or not ID_RE.fullmatch(view["id"]):
            fail("each view needs a valid id")
        if view["id"] in seen_views:
            fail(f"duplicate view id: {view['id']}")
        seen_views.add(view["id"])
        title = view.get("title")
        if not isinstance(title, str) or not title.strip() or len(title.strip()) > 80 or any(ord(char) < 32 or ord(char) == 127 for char in title):
            fail(f"view {view['id']} needs a printable title of at most 80 characters")
    viewers = manifest.get("viewers", [])
    if not isinstance(viewers, list):
        fail("manifest viewers must be a list")
    for viewer in viewers:
        if not isinstance(viewer, dict) or not viewer.get("id") or not viewer.get("title"):
            fail("each viewer needs an id and title")
        file_at(source, viewer.get("entry"), (".js", ".mjs"))
        if not viewer.get("extensions") and not viewer.get("media_types"):
            fail(f"viewer {viewer['id']} needs extensions or media_types")
    origins = manifest.get("network_origins", [])
    if not isinstance(origins, list) or len(origins) > 16:
        fail("network_origins must be a list of at most 16 HTTPS hosts")
    if len(origins) != len(set(str(origin) for origin in origins)):
        fail("network_origins contains a duplicate host")
    for origin in origins:
        if not isinstance(origin, str) or len(origin) > 253:
            fail("each network origin must be short text")
        try:
            parsed = urlsplit(origin)
            valid = (parsed.scheme == "https" and parsed.hostname and
                     not parsed.username and not parsed.password and
                     not parsed.port and not parsed.path and not parsed.query and
                     not parsed.fragment and not any(c in parsed.hostname for c in "* \\;,'\"\t\r\n") and
                     origin == "https://" + parsed.hostname.lower())
        except ValueError:
            valid = False
        if not valid:
            fail(f"invalid exact HTTPS network origin: {origin}")
    print(f"Basic Extension structure passed: {extension_id}")
    print("ELE validates the full package during installation.")


if __name__ == "__main__":
    main()
