#!/usr/bin/env python3
"""Read-only local HTML/Markdown link inventory. Python 3.9+; stdlib only.
Usage: python archive-links.py EXTRACTED_FOLDER > report.json
Exit 0: no missing/blocked/unsupported paths; 1: findings; 2: usage/read error.
Does not extract ZIPs, fetch URLs, execute HTML, or check fragment anchors.
"""
import hashlib
import json
import re
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urlsplit


class Links(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.items = []
        self.has_base = False

    def handle_starttag(self, tag, attrs):
        if tag.lower() == "base":
            self.has_base = True
        for name, value in attrs:
            if name.lower() in ("href", "src") and value is not None:
                self.items.append((name.lower(), value))


def inspect(root):
    root = Path(root).resolve(strict=True)
    if not root.is_dir():
        raise ValueError("The input must be an extracted directory")
    files = []
    references = []
    issues = []
    for path in sorted(root.rglob("*")):
        rel = path.relative_to(root).as_posix()
        if path.is_symlink():
            issues.append({"path": rel, "status": "symlink-skipped"})
            continue
        if not path.is_file():
            continue
        # Never read a file whose resolved location leaves the supplied root.
        if not path.resolve().is_relative_to(root):
            issues.append({"path": rel, "status": "outside-root"})
            continue
        data = path.read_bytes()
        files.append({"path": rel, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
        links = []
        if path.suffix.lower() in (".html", ".htm"):
            parser = Links()
            parser.feed(data.decode("utf-8-sig"))
            links = parser.items
            if parser.has_base:
                issues.append({"path": rel, "status": "html-base-unsupported"})
                # A base changes path interpretation; refuse to guess.
                continue
        elif path.suffix.lower() in (".md", ".markdown"):
            # Deliberately limited: simple inline [text](target) and ![text](target).
            content = data.decode("utf-8-sig")
            content = re.sub(r"(?ms)^```.*?^```[^\n]*", "", content)
            content = re.sub(r"`[^`\n]*`", "", content)
            links = [("markdown-inline", m.group(1)) for m in re.finditer(r"!?\[[^\]\n]*\]\(([^\s()]+)\)", content)]
        for kind, raw in links:
            row = {"source": rel, "kind": kind, "reference": raw}
            parsed = urlsplit(raw)
            if parsed.scheme or parsed.netloc:
                row["status"] = "external-or-scheme-skipped"
            elif not parsed.path:
                row["status"] = "same-page-anchor-unchecked"
            else:
                local = unquote(parsed.path)
                if local.startswith(("/", "\\")) or "\\" in local or "\x00" in local:
                    row["status"] = "unsupported-local-path"
                else:
                    target = (path.parent / local).resolve()
                    if not target.is_relative_to(root):
                        row["status"] = "outside-root"
                    else:
                        row["target"] = target.relative_to(root).as_posix()
                        row["status"] = "exists" if target.is_file() else "missing"
                        if parsed.fragment:
                            row["fragmentUnchecked"] = True
            references.append(row)
    counts = {}
    for row in references:
        counts[row["status"]] = counts.get(row["status"], 0) + 1
    return {"scope": "local file targets only; no completeness or restore verdict",
            "files": files, "references": references, "referenceCounts": counts,
            "scanIssues": issues}


if __name__ == "__main__":
    try:
        if len(sys.argv) != 2:
            raise ValueError("Usage: python archive-links.py EXTRACTED_FOLDER > report.json")
        report = inspect(sys.argv[1])
        print(json.dumps(report, indent=2, ensure_ascii=False))
        findings = bool(report["scanIssues"]) or any(
            item["status"] in ("missing", "outside-root", "unsupported-local-path")
            for item in report["references"])
        sys.exit(1 if findings else 0)
    except (OSError, UnicodeError, ValueError) as error:
        print("Archive scan failed: " + str(error), file=sys.stderr)
        sys.exit(2)
