#!/usr/bin/env python3
"""Normalize the UEC product guide as an OKF v0.2 knowledge bundle."""

from __future__ import annotations

import argparse
import posixpath
import re
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit

import yaml

ROOT = Path(__file__).resolve().parents[1]
BASE_URL = "https://platform-dev.invinsense.dev/xdr/uec/default"
SOURCE_REPO = "https://gitlab.invinsense.dev/infopercept/xdr/app-control/application-whitelisting"
OKF_SPEC = "https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md"
GENERATED_AT = "2026-10-23T00:01:00+05:30"
SOURCE_REPO_MODIFIED_AT = "2026-10-22T23:30:33+05:30"
CONSOLE_CAPTURED_AT = "2026-10-22T23:44:56+05:30"
GENERATED_BY = "codex/2026-10-22"
VERIFIED_BY = "process:uec-documentation-tests"

SECTION_TITLES = {
    "administration": "Administration",
    "app-control": "Application Control",
    "endpoint-management": "Endpoint Management",
    "get-started": "Get Started",
    "monitoring": "Monitoring and Investigation",
    "reference": "Reference",
    "release-notes": "Release Notes",
    "reporting": "Reporting",
    "resources": "Resources",
    "response": "Response and Elevated Operations",
    "risk-management": "Risk Management with AMTD",
    "security-controls": "Security Controls",
    "tests": "Documentation Tests",
    "tools": "Documentation Tools",
}

TYPE_BY_DIRECTORY = {
    "administration": "Product Guide",
    "app-control": "Product Guide",
    "endpoint-management": "Product Guide",
    "get-started": "Product Guide",
    "monitoring": "Product Guide",
    "reference": "Reference",
    "release-notes": "Release Note",
    "reporting": "Product Guide",
    "resources": "Resource",
    "response": "Product Guide",
    "risk-management": "Product Guide",
    "security-controls": "Product Guide",
    "tests": "Documentation Test",
    "tools": "Documentation Tool",
}

TYPE_BY_PATH = {
    "README.md": "Product Guide",
    "get-started/architecture.md": "Architecture Guide",
    "get-started/guided-tour.md": "Product Tour",
    "get-started/learn.md": "Learning Guide",
    "reference/api.md": "API Reference",
    "reference/event-codes.md": "Reference",
    "reference/glossary.md": "Glossary",
    "reference/kql-search.md": "Search Reference",
    "reference/open-knowledge-format.md": "Format Reference",
    "reference/roles-and-permissions.md": "Access Control Reference",
    "reference/screenshot-catalogue.md": "Screenshot Catalogue",
    "resources/datasheet.md": "Datasheet",
    "tests/README.md": "Documentation Test",
    "tools/README.md": "Documentation Tool",
}

TAGS_BY_DIRECTORY = {
    "administration": ["uec", "administration"],
    "app-control": ["uec", "app-control"],
    "endpoint-management": ["uec", "endpoints"],
    "get-started": ["uec", "getting-started"],
    "monitoring": ["uec", "monitoring"],
    "reference": ["uec", "reference"],
    "release-notes": ["uec", "release-notes"],
    "reporting": ["uec", "reporting"],
    "resources": ["uec", "resources"],
    "response": ["uec", "response"],
    "risk-management": ["uec", "amtd", "risk"],
    "security-controls": ["uec", "security-controls"],
    "tests": ["uec", "documentation", "tests"],
    "tools": ["uec", "documentation", "tooling"],
}

EXTRA_TAGS_BY_PATH = {
    "reference/api.md": ["api", "rest", "signalr"],
    "reference/event-codes.md": ["events", "event-id"],
    "reference/glossary.md": ["glossary"],
    "reference/kql-search.md": ["kql", "search"],
    "reference/open-knowledge-format.md": ["okf", "metadata"],
    "reference/roles-and-permissions.md": ["rbac", "identity"],
    "reference/screenshot-catalogue.md": ["screenshots", "light-theme", "dark-theme"],
    "resources/datasheet.md": ["datasheet"],
}

ROUTE_BY_PATH = {
    "README.md": "/",
    "get-started/getting-started.md": "/",
    "get-started/homepage.md": "/",
    "get-started/guided-tour.md": "/",
    "get-started/learn.md": "/learn/appctrl-vs-applocker",
    "reporting/reports.md": "/reports",
    "reporting/dashboard-builder.md": "/reports/builder",
    "app-control/policies.md": "/policies",
    "app-control/policy-lifecycle.md": "/policies",
    "app-control/discovery-scans.md": "/policies/wdac-supplemental/scans",
    "app-control/file-identities.md": "/file-identities",
    "app-control/software-catalog.md": "/software-catalog",
    "app-control/windows-builders.md": "/win/base-policy-builder",
    "app-control/policy-diagnostics.md": "/appctrl-tools",
    "endpoint-management/devices.md": "/devices",
    "endpoint-management/device-groups.md": "/device-groups",
    "endpoint-management/agent-deployment.md": "/agent-versions",
    "endpoint-management/platform-tools.md": "/tools",
    "endpoint-management/agent-configuration.md": "/agent-config",
    "endpoint-management/deployment-history.md": "/deployments",
    "endpoint-management/uec-intelligence.md": "/uec-intelligence",
    "monitoring/events.md": "/events",
    "monitoring/alerts.md": "/alerts",
    "monitoring/threat-detection.md": "/threat-detection",
    "monitoring/behavior-analytics.md": "/ueba",
    "monitoring/sandbox.md": "/sandbox",
    "monitoring/audit-log.md": "/audit-logs",
    "response/active-response.md": "/active-response",
    "response/domain-blocklist.md": "/global-block-domain",
    "response/just-in-time-access.md": "/jit",
    "response/forensic-queries.md": "/forensics",
    "response/live-response.md": "/live-response",
    "security-controls/defender-baselines.md": "/defender-policy",
    "security-controls/host-firewall.md": "/firewall-policy",
    "security-controls/macos-security.md": "/macos-native-security",
    "security-controls/compliance.md": "/compliance",
    "security-controls/windows-features.md": "/windows-features",
    "security-controls/certificates.md": "/certificates",
    "security-controls/file-integrity.md": "/fim/change-rules",
    "risk-management/protected-devices.md": "/amtd/devices",
    "risk-management/incidents.md": "/amtd/incidents",
    "risk-management/host-risk.md": "/amtd/hosts/inventory",
    "risk-management/vulnerabilities.md": "/amtd/vulnerabilities/application",
    "risk-management/misconfigurations.md": "/amtd/security-misconfiguration",
    "risk-management/security-controls.md": "/amtd/security-controls/operational",
    "risk-management/high-risk-software.md": "/amtd/high-risk-software",
    "risk-management/end-of-life-software.md": "/amtd/software-eol",
    "risk-management/identity-exposure.md": "/amtd/identity-exposure/logon-accounts",
    "risk-management/network-services.md": "/amtd/network-services",
    "risk-management/risk-history.md": "/amtd/risk-history",
    "risk-management/data-exfiltration.md": "/amtd/data-exfiltration",
    "risk-management/ai-usage-control.md": "/amtd/ai-usage-control/ai-tool",
    "risk-management/software-inventory.md": "/amtd/software/software-applications",
    "administration/settings.md": "/settings",
    "administration/directory-groups.md": "/groups",
    "administration/platform-health.md": "/about",
}

DESCRIPTION_BY_PATH = {
    "README.md": "Invinsense UEC product guide, organized by operator outcome from first sign-in through endpoint risk management.",
    "reference/open-knowledge-format.md": "How this repository maps product documentation to Open Knowledge Format v0.2 concepts, indexes, links, and provenance.",
    "reference/screenshot-catalogue.md": "Index of 94 unique UEC pages captured in both light and dark themes after a clean v7.6.0 pod redeploy.",
    "get-started/README.md": "Foundational UEC concepts, architecture, first sign-in, homepage orientation, guided tour, and built-in learning pages.",
    "reference/README.md": "Compact reference hub for KQL search, permissions, event codes, API conventions, glossary, OKF mapping, and screenshots.",
    "release-notes/README.md": "Index to published UEC release notes and the relationship between release documentation and newer development screenshots.",
    "resources/README.md": "Evaluation and procurement resources built from the UEC product guide.",
    "tests/README.md": "Instructions and guarantees for the product documentation structure and screenshot test suite.",
    "tools/README.md": "Canonical screenshot route manifest, safe capture policy, and documentation tooling for the UEC knowledge bundle.",
}


class _NoAliasDumper(yaml.SafeDumper):
    def ignore_aliases(self, data):
        return True


def strip_frontmatter(text: str) -> tuple[str, dict]:
    if not text.startswith("---\n"):
        return text, {}
    parts = text.split("\n---\n", 1)
    if len(parts) != 2:
        return text, {}
    try:
        metadata = yaml.safe_load(parts[0][4:]) or {}
    except yaml.YAMLError:
        metadata = {}
    return parts[1].lstrip("\n"), metadata if isinstance(metadata, dict) else {}


def clean_inline(value: str) -> str:
    value = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", value)
    value = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", value)
    value = value.replace("`", "")
    value = re.sub(r"[*_]+", "", value)
    return re.sub(r"\s+", " ", value).strip()


def derive_title(body: str) -> str:
    match = re.search(r"^# (.+)$", body, flags=re.MULTILINE)
    return clean_inline(match.group(1)) if match else "Untitled concept"


def derive_description(body: str, relative_path: Path) -> str:
    if relative_path.as_posix() in DESCRIPTION_BY_PATH:
        return DESCRIPTION_BY_PATH[relative_path.as_posix()]
    lines = body.splitlines()
    start = next((i for i, line in enumerate(lines) if line.startswith("# ")), -1)
    candidate_lines = lines[start + 1 :] if start >= 0 else lines
    paragraph: list[str] = []
    in_fence = False
    for line in candidate_lines:
        stripped = line.strip()
        if stripped.startswith("```"):
            in_fence = not in_fence
            continue
        if in_fence:
            continue
        if not stripped:
            if paragraph:
                break
            continue
        if paragraph and stripped.startswith("#"):
            break
        if not paragraph and (not stripped or stripped.startswith(("![", "<p", "[!", "---", "|", ">", "#"))):
            continue
        if not paragraph and stripped.startswith(("**Released", "**Latest", "**Server", "**Compatible")):
            continue
        paragraph.append(stripped)
    sentence = clean_inline(" ".join(paragraph)) if paragraph else f"UEC product documentation for {derive_title(body)}."
    match = re.match(r"^(.*?[.!?])(?:\s|$)", sentence)
    if match:
        sentence = match.group(1)
    sentence = sentence.rstrip(":;") + "." if sentence.endswith((":", ";")) else sentence
    if len(sentence) > 240:
        sentence = sentence[:237].rstrip() + "..."
    return sentence


def bundle_href(current_relative: Path, target: str, *, add_index: bool = True) -> str:
    parsed = urlsplit(target)
    if parsed.scheme or parsed.netloc or target.startswith(("mailto:", "#")):
        return target
    current_dir = current_relative.parent.as_posix()
    path = parsed.path or "/"
    resolved = posixpath.normpath(posixpath.join(current_dir, path))
    if add_index and (resolved.endswith("/") or "." not in posixpath.basename(resolved)):
        candidate = ROOT / resolved
        if candidate.is_dir():
            resolved = posixpath.join(resolved, "index.md")
    if not resolved.startswith("/"):
        resolved = "/" + resolved
    return urlunsplit(("", "", resolved, parsed.query, parsed.fragment))


def normalize_links(body: str, relative_path: Path) -> str:
    def replace_markdown(match: re.Match[str]) -> str:
        label, target = match.group(1), match.group(2)
        return f"[{label}]({bundle_href(relative_path, target)})"

    body = re.sub(r"\[([^\]]+)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)", replace_markdown, body)

    def replace_html(match: re.Match[str]) -> str:
        return f'src="{bundle_href(relative_path, match.group(1), add_index=False)}"'

    return re.sub(r'src="([^"]+)"', replace_html, body)


def concept_type(relative_path: Path) -> str:
    posix = relative_path.as_posix()
    if posix in TYPE_BY_PATH:
        return TYPE_BY_PATH[posix]
    first = relative_path.parts[0]
    if relative_path.name == "README.md":
        return "Documentation Section"
    return TYPE_BY_DIRECTORY.get(first, "Product Guide")


def concept_tags(relative_path: Path) -> list[str]:
    first = relative_path.parts[0]
    tags = ["uec"] if relative_path == Path("README.md") else list(TAGS_BY_DIRECTORY.get(first, ["uec"]))
    tags.extend(EXTRA_TAGS_BY_PATH.get(relative_path.as_posix(), []))
    return list(dict.fromkeys(tags))


def concept_resource(relative_path: Path) -> str | None:
    route = ROUTE_BY_PATH.get(relative_path.as_posix())
    return BASE_URL + route if route is not None else None


def source_family() -> list[dict]:
    return [
        {
            "id": "uec-source-repository",
            "resource": SOURCE_REPO,
            "title": "Unified Endpoint Control application-whitelisting repository",
            "last_modified": SOURCE_REPO_MODIFIED_AT,
        },
        {
            "id": "uec-console-v7-6-0",
            "resource": BASE_URL,
            "title": "UEC development console v7.6.0 after pod redeploy",
            "last_modified": GENERATED_AT,
        },
        {
            "id": "okf-v0-2-spec",
            "resource": OKF_SPEC,
            "title": "Open Knowledge Format v0.2 specification",
        },
    ]


def concept_metadata(relative_path: Path, body: str, previous: dict) -> dict:
    metadata = {
        "type": concept_type(relative_path),
        "title": derive_title(body),
        "description": derive_description(body, relative_path),
    }
    resource = previous.get("resource") or concept_resource(relative_path)
    if resource:
        metadata["resource"] = resource
    metadata["tags"] = previous.get("tags") or concept_tags(relative_path)
    metadata["generated"] = previous.get("generated") or {
        "by": GENERATED_BY,
        "at": GENERATED_AT,
    }
    metadata["verified"] = previous.get("verified") or {
        "by": VERIFIED_BY,
        "at": GENERATED_AT,
    }
    metadata["status"] = previous.get("status") or "stable"
    metadata["sources"] = previous.get("sources") or source_family()
    return metadata


def write_concept(path: Path) -> dict:
    relative = path.relative_to(ROOT)
    text = path.read_text(encoding="utf-8")
    body, previous = strip_frontmatter(text)
    body = normalize_links(body, relative)
    metadata = concept_metadata(relative, body, previous)
    rendered = yaml.dump(
        metadata,
        Dumper=_NoAliasDumper,
        sort_keys=False,
        allow_unicode=True,
        width=110,
        default_flow_style=False,
    ).rstrip()
    path.write_text(f"---\n{rendered}\n---\n\n{body.lstrip()}", encoding="utf-8")
    return metadata


def load_concept(path: Path) -> dict:
    _, metadata = strip_frontmatter(path.read_text(encoding="utf-8"))
    return metadata


def render_index(directory: Path, is_root: bool) -> None:
    entries: list[tuple[str, str, str]] = []
    child_dirs: list[tuple[str, str]] = []
    for child in sorted(directory.iterdir()):
        if child.name in {"index.md", "log.md"} or child.name.startswith("."):
            continue
        if child.is_dir():
            child_index = child / "index.md"
            if child_index.exists():
                metadata = load_concept(child / "README.md") if (child / "README.md").exists() else {}
                child_dirs.append((child.name, metadata.get("description", f"{child.name} concepts.")))
        elif child.suffix == ".md":
            metadata = load_concept(child)
            entries.append((metadata.get("title", child.stem), child.name, metadata.get("description", "")))

    if directory.relative_to(ROOT).as_posix() == "release-notes":
        def release_sort(item):
            title, filename, _ = item
            if filename == "README.md": return (0, "")
            if filename == "latest.md": return (1, "")
            metadata = load_concept(directory / filename)
            sources = metadata.get("sources", []) if isinstance(metadata.get("sources"), list) else []
            timestamp = ""
            if sources and isinstance(sources[0], dict):
                timestamp = str(sources[0].get("last_modified", ""))
            return (2, timestamp)
        entries.sort(key=release_sort, reverse=True)

    if is_root:
        lines = [
            "---",
            'okf_version: "0.2"',
            "---",
            "",
            "# UEC Product Knowledge Bundle",
            "",
            "This bundle is the complete Invinsense Unified Endpoint Control product guide in Open Knowledge Format v0.2.",
            "",
            "# Product Guide",
            "",
            "* [Invinsense UEC Product Guide](README.md) - Complete audience-organized product guide and navigation hub.",
            "* [Update Log](log.md) - OKF bundle creation and maintenance history.",
            "",
            "# Knowledge Domains",
            "",
        ]
        product_domains = [
            "get-started", "app-control", "endpoint-management", "monitoring", "response",
            "security-controls", "risk-management", "reporting", "administration",
        ]
        child_dir_by_slug = dict(child_dirs)
        for slug in product_domains:
            description = child_dir_by_slug[slug]
            title = SECTION_TITLES[slug]
            lines.append(f"* [{title}]({slug}/index.md) - {description}")
        lines.extend([
            "",
            "# Documentation Operations",
            "",
        ])
        for slug in ("reference", "release-notes", "resources", "tests", "tools"):
            match = next((x for x in child_dirs if x[0] == slug), None)
            if match:
                lines.append(f"* [{SECTION_TITLES[slug]}]({slug}/index.md) - {match[1]}")
    else:
        slug = directory.relative_to(ROOT).as_posix()
        title = SECTION_TITLES.get(slug, directory.name.replace("-", " ").title())
        lines = [f"# {title} Concepts", ""]
        for concept_title, filename, description in entries:
            lines.append(f"* [{concept_title}]({filename}) - {description}")
        if not entries:
            lines.append("* No concept documents.")

    (directory / "index.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")


def write_log() -> None:
    text = """# UEC Documentation Update Log

## 2026-10-23
* **Update**: Normalized the guide as an Open Knowledge Format v0.2 bundle with concept frontmatter, provenance, lifecycle status, machine verification, bundle-relative links, and progressive-disclosure indexes.
* **Update**: Added an Astro Starlight static browser site with sidebar navigation, search, light/dark mode, and a deterministic `/uec/` build pipeline.
* **Verification**: Validated OKF conformance, Markdown structure, local links, image alignment, image sets, image dimensions, image budgets, and generated-site links.

## 2026-10-22
* **Update**: Rebuilt all screenshots after a clean UEC v7.6.0 pod redeploy using one browser session, canonical routes, a 15-second load gap, and the real light/dark switch.
* **Creation**: Established the complete audience-organized UEC product guide.
"""
    (ROOT / "log.md").write_text(text, encoding="utf-8")


def is_canonical_markdown(path: Path) -> bool:
    ignored_roots = {"site", "dist", ".astro", "node_modules", "public"}
    ignored_exact = {"src/content/docs"}
    rel = path.relative_to(ROOT)
    if ".git" in rel.parts or any(part in ignored_roots for part in rel.parts):
        return False
    if len(rel.parts) >= 3 and "/".join(rel.parts[:3]) == "src/content/docs":
        return False
    return path.name not in {"index.md", "log.md"}


def build(write: bool = True) -> list[Path]:
    concept_paths = [path for path in ROOT.rglob("*.md") if is_canonical_markdown(path)]
    if write:
        write_log()
    for path in sorted(concept_paths):
        write_concept(path)
    for directory in sorted({path.parent for path in ROOT.rglob("*.md") if is_canonical_markdown(path)}, reverse=True):
        if directory == ROOT or any(parent.name == ".git" for parent in directory.parents):
            continue
        render_index(directory, is_root=False)
    render_index(ROOT, is_root=True)
    return concept_paths


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.parse_args()
    paths = build()
    print(f"Normalized {len(paths)} OKF concepts and generated progressive-disclosure indexes.")


if __name__ == "__main__":
    main()
