#!/usr/bin/env python3
import argparse
import hashlib
import html
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple

import yaml


HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"]
AUTOGEN_NOTE = "AUTO-GENERATED, DO NOT EDIT"
TOC_START_MARKER = "<!-- AUTO-GENERATED API TOC START -->"
TOC_END_MARKER = "<!-- AUTO-GENERATED API TOC END -->"
OPS_PER_PAGE = 20
MODULE_DESCRIPTIONS: Dict[str, Tuple[str, str]] = {
    "action-logs": ("Action Logs", "Audit and operational logs for integration module activity."),
    "backup": ("Backup", "Backup integrations and backup module orchestration."),
    "cvr": ("CVR", "Danish company registry (CVR) lookup and search integration."),
    "economic": ("e-conomic", "Accounting and invoicing integration with e-conomic."),
    "entra": ("Entra", "Microsoft Entra directory integration endpoints."),
    "fxratesapi": ("FXRatesAPI", "Currency exchange-rate lookup integration."),
    "motorapi": ("MotorAPI", "Vehicle lookup integration via MotorAPI."),
    "self-serve": ("Self-Serve", "Self-serve lane control and machine command endpoints."),
    "stripe": ("Stripe", "Stripe payments, invoices, terminals, products, and customers."),
    "virkdata": ("VirkData", "VirkData company information integration."),
    "washcertificates": ("Wash Certificates", "Wash certificate retrieval and listing integration."),
    "weatherapi": ("WeatherAPI", "Weather provider integration for current, forecast, and search."),
    "xlvask": ("XLVask", "XLVask synchronization, usage logs, vehicles, and customers."),
}
CONFIG_DESCRIPTIONS: Dict[str, Tuple[str, str]] = {
    "backups": ("Backups", "Backup configuration for backup module behavior."),
    "bird": ("Bird", "Bird communication integration configuration."),
    "economic": ("e-conomic", "e-conomic accounting integration configuration."),
    "email": ("Email", "Email provider and SMTP/MailerSend configuration."),
    "entra": ("Entra", "Microsoft Entra identity integration configuration."),
    "fxratesapi": ("FXRatesAPI", "FXRatesAPI exchange-rate integration configuration."),
    "gatewayapi": ("GatewayAPI", "GatewayAPI integration configuration."),
    "licenseplaterecognizer": ("LicensePlateRecognizer", "License plate recognizer integration configuration."),
    "limble": ("Limble", "Limble integration configuration."),
    "motorapi": ("MotorAPI", "MotorAPI vehicle lookup integration configuration."),
    "ocrspace": ("OcrSpace", "OCR Space integration configuration."),
    "openai": ("OpenAI", "OpenAI integration configuration."),
    "reCAPTCHA": ("reCAPTCHA", "reCAPTCHA protection configuration."),
    "selfserve": ("Self-Serve", "Self-serve module runtime configuration."),
    "shelly": ("Shelly", "Shelly integration configuration."),
    "stripe": ("Stripe", "Stripe integration configuration."),
    "virkdata": ("VirkData", "VirkData integration configuration."),
    "weatherapi": ("WeatherAPI", "WeatherAPI integration configuration."),
    "xlvask": ("XLVask", "XLVask integration configuration."),
}


@dataclass(frozen=True)
class Operation:
    method: str
    path: str
    title: str
    topic_id: str
    topic_file: str
    primary_tag: str
    operation_id: str
    description: str
    parameters: List[dict]
    request_body: dict
    responses: dict
    security: List[dict]
    module_key: str
    module_name: str
    module_description: str
    config_key: str
    config_name: str
    config_description: str


def slugify(value: str) -> str:
    slug = re.sub(r"[^A-Za-z0-9_]+", "_", value.strip())
    slug = re.sub(r"_+", "_", slug).strip("_")
    if not slug:
        slug = "unnamed"
    if not re.match(r"^[A-Za-z_]", slug):
        slug = f"id_{slug}"
    return slug


def safe_token(value: str) -> str:
    return slugify(value).lower()


def build_operation_topic_id(operation_id: str, method: str, path: str) -> str:
    base = slugify(operation_id)
    if len(base) > 110:
        digest = hashlib.sha1(f"{method}:{path}:{operation_id}".encode("utf-8")).hexdigest()[:8]
        base = f"{base[:100]}_{digest}"
    return base


def schema_type_name(schema: dict) -> str:
    if not isinstance(schema, dict):
        return "unknown"
    if "$ref" in schema:
        ref = str(schema["$ref"])
        return ref.rsplit("/", 1)[-1]
    if "type" in schema:
        t = str(schema["type"])
        if t == "array" and isinstance(schema.get("items"), dict):
            return f"array<{schema_type_name(schema['items'])}>"
        return t
    if "oneOf" in schema:
        return "oneOf"
    if "anyOf" in schema:
        return "anyOf"
    if "allOf" in schema:
        return "allOf"
    return "object"


def xml_escape(value: str) -> str:
    return html.escape(value or "", quote=True)


def pretty_json(value: dict) -> str:
    return html.escape(json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True))


def render_parameters_table(parameters: List[dict]) -> str:
    if not parameters:
        return ""
    rows = [
        "    <chapter title=\"Parameters\" id=\"parameters\">",
        "        <table>",
        "            <tr><td>Name</td><td>In</td><td>Required</td><td>Type</td><td>Description</td></tr>",
    ]
    for param in parameters:
        name = xml_escape(str(param.get("name", "")))
        loc = xml_escape(str(param.get("in", "")))
        required = "yes" if param.get("required") else "no"
        ptype = xml_escape(schema_type_name(param.get("schema", {})))
        desc = xml_escape(str(param.get("description", "")))
        rows.append(
            f"            <tr><td>{name}</td><td>{loc}</td><td>{required}</td><td>{ptype}</td><td>{desc}</td></tr>"
        )
    rows.append("        </table>")
    rows.append("    </chapter>")
    return "\n".join(rows) + "\n"


def render_request_body(request_body: dict) -> str:
    if not isinstance(request_body, dict) or not request_body:
        return ""
    lines = ["    <chapter title=\"Request Body\" id=\"request-body\">"]
    lines.append(f"        <p>Required: {'yes' if request_body.get('required') else 'no'}.</p>")
    content = request_body.get("content", {})
    if isinstance(content, dict) and content:
        for content_type, media in content.items():
            lines.append(f"        <p>Content type: <code>{xml_escape(str(content_type))}</code></p>")
            schema = media.get("schema") if isinstance(media, dict) else None
            if isinstance(schema, dict):
                lines.append("        <code-block lang=\"json\">")
                lines.append(pretty_json(schema))
                lines.append("        </code-block>")
    lines.append("    </chapter>")
    return "\n".join(lines) + "\n"


def render_responses(responses: dict) -> str:
    if not isinstance(responses, dict) or not responses:
        return ""
    lines = [
        "    <chapter title=\"Responses\" id=\"responses\">",
        "        <table>",
        "            <tr><td>Status</td><td>Description</td><td>Content Types</td></tr>",
    ]
    for status_code, response in responses.items():
        if not isinstance(response, dict):
            continue
        description = xml_escape(str(response.get("description", "")))
        content = response.get("content", {})
        if isinstance(content, dict):
            content_types = ", ".join(sorted(str(k) for k in content.keys()))
        else:
            content_types = ""
        lines.append(
            f"            <tr><td>{xml_escape(str(status_code))}</td><td>{description}</td><td>{xml_escape(content_types)}</td></tr>"
        )
    lines.append("        </table>")

    for status_code, response in responses.items():
        if not isinstance(response, dict):
            continue
        content = response.get("content", {})
        if not isinstance(content, dict):
            continue
        for content_type, media in content.items():
            schema = media.get("schema") if isinstance(media, dict) else None
            if not isinstance(schema, dict):
                continue
            lines.append(
                f"        <p>Schema for response <code>{xml_escape(str(status_code))}</code> (<code>{xml_escape(str(content_type))}</code>):</p>"
            )
            lines.append("        <code-block lang=\"json\">")
            lines.append(pretty_json(schema))
            lines.append("        </code-block>")
    lines.append("    </chapter>")
    return "\n".join(lines) + "\n"


def render_security(security: List[dict]) -> str:
    if not security:
        return "    <chapter title=\"Authentication\" id=\"authentication\"><p>No authentication required.</p></chapter>\n"
    lines = [
        "    <chapter title=\"Authentication\" id=\"authentication\">",
        "        <p>Security requirements:</p>",
        "        <table>",
        "            <tr><td>Scheme</td><td>Scopes</td></tr>",
    ]
    for req in security:
        if not isinstance(req, dict):
            continue
        for scheme, scopes in req.items():
            if isinstance(scopes, list):
                scope_text = ", ".join(str(s) for s in scopes) if scopes else "-"
            else:
                scope_text = "-"
            lines.append(
                f"            <tr><td>{xml_escape(str(scheme))}</td><td>{xml_escape(scope_text)}</td></tr>"
            )
    lines.extend(["        </table>", "    </chapter>"])
    return "\n".join(lines) + "\n"


def render_operation_topic(operation: Operation) -> str:
    title = html.escape(operation.title, quote=True)
    endpoint = xml_escape(operation.path)
    method = operation.method.upper()
    topic_id = html.escape(operation.topic_id, quote=True)
    description = xml_escape(operation.description)
    operation_id = xml_escape(operation.operation_id)

    parameter_block = render_parameters_table(operation.parameters)
    request_block = render_request_body(operation.request_body)
    response_block = render_responses(operation.responses)
    security_block = render_security(operation.security)

    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<!DOCTYPE topic\n'
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">\n'
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"\n'
        f'       title="{title}" id="{topic_id}">\n'
        f"\n    <!-- {AUTOGEN_NOTE} -->\n"
        "    <p>This endpoint documentation is generated directly from <code>openapi.yaml</code>.</p>\n"
        "    <chapter title=\"Endpoint\" id=\"endpoint\">\n"
        f"        <code-block lang=\"http\">{method} {endpoint}</code-block>\n"
        "    </chapter>\n"
        "    <chapter title=\"Operation\" id=\"operation\">\n"
        f"        <p>Operation ID: <code>{operation_id}</code></p>\n"
        f"        <p>{description}</p>\n"
        "    </chapter>\n"
        f"{security_block}"
        f"{parameter_block}"
        f"{request_block}"
        f"{response_block}"
        "</topic>\n"
    )


def render_tag_topic(tag: str, topic_id: str) -> str:
    title = html.escape(tag, quote=True)
    safe_id = html.escape(topic_id, quote=True)
    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<!DOCTYPE topic\n'
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">\n'
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"\n'
        f'       title="{title}" id="{safe_id}">\n'
        f"\n    <!-- {AUTOGEN_NOTE} -->\n"
        "    <p>Endpoints in this section are generated from <code>openapi.yaml</code>.</p>\n"
        "</topic>\n"
    )


def render_tag_page_topic(tag: str, topic_id: str, page_number: int, total_pages: int) -> str:
    title = html.escape(f"{tag} - Page {page_number} of {total_pages}", quote=True)
    safe_id = html.escape(topic_id, quote=True)
    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<!DOCTYPE topic\n'
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">\n'
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"\n'
        f'       title="{title}" id="{safe_id}">\n'
        f"\n    <!-- {AUTOGEN_NOTE} -->\n"
        "    <p>This page groups endpoint topics for this object type.</p>\n"
        "</topic>\n"
    )


def render_module_topic(module_name: str, module_description: str, topic_id: str) -> str:
    title = html.escape(module_name, quote=True)
    safe_id = html.escape(topic_id, quote=True)
    description = xml_escape(module_description)
    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<!DOCTYPE topic\n'
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">\n'
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"\n'
        f'       title="{title}" id="{safe_id}">\n'
        f"\n    <!-- {AUTOGEN_NOTE} -->\n"
        f"    <p>{description}</p>\n"
        "</topic>\n"
    )


def render_modules_index_topic(topic_id: str, modules: List[Tuple[str, str]]) -> str:
    safe_id = html.escape(topic_id, quote=True)
    rows = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        "<!DOCTYPE topic",
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">',
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"',
        f'       title="Modules" id="{safe_id}">',
        "",
        f"    <!-- {AUTOGEN_NOTE} -->",
        "    <p>Module integrations sorted by module name.</p>",
        "    <chapter title=\"Module Catalog\" id=\"module-catalog\">",
        "        <table>",
        "            <tr><td>Module</td><td>Description</td></tr>",
    ]
    for module_name, module_description in modules:
        rows.append(
            f"            <tr><td>{xml_escape(module_name)}</td><td>{xml_escape(module_description)}</td></tr>"
        )
    rows.extend(["        </table>", "    </chapter>", "</topic>", ""])
    return "\n".join(rows)


def render_config_index_topic(topic_id: str, modules: List[Tuple[str, str]]) -> str:
    safe_id = html.escape(topic_id, quote=True)
    rows = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        "<!DOCTYPE topic",
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">',
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"',
        f'       title="Config" id="{safe_id}">',
        "",
        f"    <!-- {AUTOGEN_NOTE} -->",
        "    <p>Configuration endpoints grouped by module name.</p>",
        "    <chapter title=\"Configuration Module Catalog\" id=\"config-module-catalog\">",
        "        <table>",
        "            <tr><td>Module</td><td>Description</td></tr>",
    ]
    for module_name, module_description in modules:
        rows.append(
            f"            <tr><td>{xml_escape(module_name)}</td><td>{xml_escape(module_description)}</td></tr>"
        )
    rows.extend(["        </table>", "    </chapter>", "</topic>", ""])
    return "\n".join(rows)


def infer_module(path: str) -> Tuple[str, str, str]:
    segments = [segment for segment in path.split("/") if segment]
    module_key = "misc"
    if segments:
        if segments[0] == "modules" and len(segments) > 1:
            module_key = segments[1]
        elif segments[0] in {"economic", "cvr"}:
            module_key = segments[0]

    if module_key in MODULE_DESCRIPTIONS:
        module_name, module_description = MODULE_DESCRIPTIONS[module_key]
    else:
        module_name = module_key.replace("-", " ").title()
        module_description = "Integration module endpoints."
    return module_key, module_name, module_description


def infer_config_module(path: str) -> Tuple[str, str, str]:
    segments = [segment for segment in path.split("/") if segment]
    config_key = segments[0] if segments else "misc"
    if config_key in CONFIG_DESCRIPTIONS:
        config_name, config_description = CONFIG_DESCRIPTIONS[config_key]
    else:
        config_name = config_key.replace("-", " ").title()
        config_description = "Configuration endpoints for this module."
    return config_key, config_name, config_description


def render_api_reference_topic() -> str:
    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<!DOCTYPE topic\n'
        '        SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd">\n'
        '<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
        '       xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"\n'
        '       title="API Reference" id="API-Reference">\n'
        f"\n    <!-- {AUTOGEN_NOTE} -->\n"
        "    <p>Comprehensive API reference generated from the repository root <code>openapi.yaml</code>.</p>\n"
        "</topic>\n"
    )


def write_if_changed(path: Path, content: str) -> bool:
    if path.exists() and path.read_text(encoding="utf-8") == content:
        return False
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")
    return True


def parse_openapi(openapi_path: Path) -> Tuple[List[str], List[Operation], Dict[str, List[Operation]]]:
    doc = yaml.safe_load(openapi_path.read_text(encoding="utf-8"))
    global_security = doc.get("security", [])
    tags_section = doc.get("tags", [])
    declared_tags = []
    for tag_item in tags_section:
        if isinstance(tag_item, dict) and isinstance(tag_item.get("name"), str):
            declared_tags.append(tag_item["name"])

    declared_tag_set = set(declared_tags)
    paths = doc.get("paths", {})
    if not isinstance(paths, dict):
        raise ValueError("Invalid OpenAPI: top-level 'paths' must be an object.")

    operations: List[Operation] = []
    grouped: Dict[str, List[Operation]] = {}
    seen_topic_ids: Dict[str, str] = {}
    unknown_tags: List[str] = []

    for api_path, path_item in paths.items():
        if not isinstance(path_item, dict):
            continue
        path_parameters = path_item.get("parameters", [])
        for method in HTTP_METHODS:
            operation = path_item.get(method)
            if not isinstance(operation, dict):
                continue

            operation_tags = operation.get("tags")
            if isinstance(operation_tags, list) and operation_tags:
                tags = [str(tag) for tag in operation_tags]
            else:
                tags = ["Misc"]

            for tag in tags:
                if tag != "Misc" and tag not in declared_tag_set:
                    unknown_tags.append(f"{method.upper()} {api_path} => '{tag}'")

            primary_tag = tags[0]
            operation_id = operation.get("operationId")
            if not isinstance(operation_id, str) or not operation_id.strip():
                operation_id = f"{method}_{api_path}"

            summary = operation.get("summary")
            if not isinstance(summary, str) or not summary.strip():
                summary = f"{method.upper()} {api_path}"
            description = operation.get("description")
            if not isinstance(description, str) or not description.strip():
                description = summary

            topic_id = build_operation_topic_id(operation_id, method, api_path)
            existing = seen_topic_ids.get(topic_id)
            operation_ref = f"{method.upper()} {api_path}"
            if existing and existing != operation_ref:
                raise ValueError(
                    f"Duplicate topic id '{topic_id}' generated for '{existing}' and '{operation_ref}'."
                )
            seen_topic_ids[topic_id] = operation_ref

            topic_file = f"{topic_id}.topic"
            operation_parameters = operation.get("parameters", [])
            merged_parameters: List[dict] = []
            seen_params = set()
            for param_source in [path_parameters, operation_parameters]:
                if not isinstance(param_source, list):
                    continue
                for param in param_source:
                    if not isinstance(param, dict):
                        continue
                    key = (str(param.get("in", "")), str(param.get("name", "")))
                    if key in seen_params:
                        merged_parameters = [
                            p
                            for p in merged_parameters
                            if (str(p.get("in", "")), str(p.get("name", ""))) != key
                        ]
                    seen_params.add(key)
                    merged_parameters.append(param)

            module_key, module_name, module_description = infer_module(api_path)
            config_key, config_name, config_description = infer_config_module(api_path)
            op = Operation(
                method=method,
                path=api_path,
                title=summary,
                topic_id=topic_id,
                topic_file=topic_file,
                primary_tag=primary_tag,
                operation_id=operation_id,
                description=description,
                parameters=merged_parameters,
                request_body=operation.get("requestBody", {}),
                responses=operation.get("responses", {}),
                security=operation.get("security", global_security),
                module_key=module_key,
                module_name=module_name,
                module_description=module_description,
                config_key=config_key,
                config_name=config_name,
                config_description=config_description,
            )
            operations.append(op)
            grouped.setdefault(primary_tag, []).append(op)

    if unknown_tags:
        raise ValueError(
            "Invalid tag mapping: operation tags not declared in top-level OpenAPI tags:\n"
            + "\n".join(sorted(unknown_tags))
        )

    for tag_ops in grouped.values():
        tag_ops.sort(key=lambda op: (op.path, op.method))

    ordered_tags = [tag for tag in declared_tags if tag in grouped]
    if "Misc" in grouped and "Misc" not in ordered_tags:
        ordered_tags.append("Misc")
    for extra in sorted(tag for tag in grouped if tag not in ordered_tags):
        ordered_tags.append(extra)

    return ordered_tags, operations, grouped


def render_generated_toc(ordered_tags: List[str], grouped: Dict[str, List[Operation]]) -> str:
    lines: List[str] = []
    lines.append('    <toc-element topic="API-Reference.topic">')
    for tag in ordered_tags:
        tag_topic_file = f"Tag_{slugify(tag)}.topic"
        lines.append(f'        <toc-element topic="{tag_topic_file}">')
        tag_ops = grouped.get(tag, [])
        if tag == "Modules":
            module_groups: Dict[str, List[Operation]] = {}
            module_meta: Dict[str, Tuple[str, str]] = {}
            for op in tag_ops:
                module_groups.setdefault(op.module_key, []).append(op)
                module_meta[op.module_key] = (op.module_name, op.module_description)

            for module_key in sorted(module_groups, key=lambda key: module_meta[key][0].lower()):
                module_name, _ = module_meta[module_key]
                module_slug = safe_token(module_name)
                module_topic = f"modules_module_{module_slug}.topic"
                lines.append(f'            <toc-element topic="{module_topic}">')
                module_ops = sorted(module_groups[module_key], key=lambda op: (op.module_name.lower(), op.title.lower(), op.path, op.method))
                total_pages = max(1, (len(module_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
                for page_index in range(total_pages):
                    start = page_index * OPS_PER_PAGE
                    end = start + OPS_PER_PAGE
                    page_ops = module_ops[start:end]
                    page_number = page_index + 1
                    page_topic = f"modules_module_{module_slug}_page_{page_number}.topic"
                    lines.append(f'                <toc-element topic="{page_topic}">')
                    for op in page_ops:
                        lines.append(f'                    <toc-element topic="{op.topic_file}"/>')
                    lines.append("                </toc-element>")
                lines.append("            </toc-element>")
            lines.append("        </toc-element>")
            continue

        if tag == "Config":
            config_groups: Dict[str, List[Operation]] = {}
            config_meta: Dict[str, Tuple[str, str]] = {}
            for op in tag_ops:
                config_groups.setdefault(op.config_key, []).append(op)
                config_meta[op.config_key] = (op.config_name, op.config_description)

            for config_key in sorted(config_groups, key=lambda key: config_meta[key][0].lower()):
                config_name, _ = config_meta[config_key]
                config_slug = safe_token(config_name)
                config_topic = f"config_module_{config_slug}.topic"
                lines.append(f'            <toc-element topic="{config_topic}">')
                config_ops = sorted(config_groups[config_key], key=lambda op: (op.title.lower(), op.path, op.method))
                total_pages = max(1, (len(config_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
                for page_index in range(total_pages):
                    start = page_index * OPS_PER_PAGE
                    end = start + OPS_PER_PAGE
                    page_ops = config_ops[start:end]
                    page_number = page_index + 1
                    page_topic = f"config_module_{config_slug}_page_{page_number}.topic"
                    lines.append(f'                <toc-element topic="{page_topic}">')
                    for op in page_ops:
                        lines.append(f'                    <toc-element topic="{op.topic_file}"/>')
                    lines.append("                </toc-element>")
                lines.append("            </toc-element>")
            lines.append("        </toc-element>")
            continue

        total_pages = max(1, (len(tag_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
        for page_index in range(total_pages):
            start = page_index * OPS_PER_PAGE
            end = start + OPS_PER_PAGE
            page_ops = tag_ops[start:end]
            page_number = page_index + 1
            page_topic = f"Tag_{slugify(tag)}_Page_{page_number}.topic"
            lines.append(f'            <toc-element topic="{page_topic}">')
            for op in page_ops:
                lines.append(f'                <toc-element topic="{op.topic_file}"/>')
            lines.append("            </toc-element>")
        lines.append("        </toc-element>")
    lines.append("    </toc-element>")
    return "\n".join(lines) + "\n"


def apply_toc_block(ctw_tree_content: str, generated_toc: str) -> str:
    block = f"    {TOC_START_MARKER}\n{generated_toc.rstrip()}\n    {TOC_END_MARKER}\n"
    marker_pattern = re.compile(
        rf"(?ms)^[ \t]*{re.escape(TOC_START_MARKER)}\r?\n.*?^[ \t]*{re.escape(TOC_END_MARKER)}\r?\n?"
    )
    if marker_pattern.search(ctw_tree_content):
        return marker_pattern.sub(block, ctw_tree_content, count=1)

    close_tag = "</instance-profile>"
    close_index = ctw_tree_content.rfind(close_tag)
    if close_index == -1:
        raise ValueError("Unable to update ctw.tree: missing </instance-profile>.")
    left = ctw_tree_content[:close_index].rstrip() + "\n\n"
    right = ctw_tree_content[close_index:]
    return left + block + right


def build_expected_outputs(root: Path) -> Dict[Path, str]:
    documentation_dir = root / "documentation"
    openapi_path = root / "openapi.yaml"
    ctw_tree_path = documentation_dir / "ctw.tree"
    api_reference_topic_path = documentation_dir / "topics" / "API-Reference.topic"
    generated_dir = documentation_dir / "topics" / "generated"
    generated_toc_path = documentation_dir / "generated" / "api-reference.toc.xml"

    openapi_doc = yaml.safe_load(openapi_path.read_text(encoding="utf-8"))
    ordered_tags, operations, grouped = parse_openapi(openapi_path)
    generated_toc = render_generated_toc(ordered_tags, grouped)

    expected: Dict[Path, str] = {}
    expected[generated_toc_path] = generated_toc
    expected[documentation_dir / "generated" / "openapi.json"] = json.dumps(
        openapi_doc, indent=2, ensure_ascii=False, sort_keys=False
    ) + "\n"
    expected[api_reference_topic_path] = render_api_reference_topic()

    existing_tree = ctw_tree_path.read_text(encoding="utf-8")
    expected[ctw_tree_path] = apply_toc_block(existing_tree, generated_toc)

    for tag in ordered_tags:
        tag_slug = slugify(tag)
        tag_topic_file = generated_dir / f"Tag_{tag_slug}.topic"
        tag_ops = grouped[tag]
        if tag == "Modules":
            module_groups: Dict[str, List[Operation]] = {}
            module_meta: Dict[str, Tuple[str, str]] = {}
            for op in tag_ops:
                module_groups.setdefault(op.module_key, []).append(op)
                module_meta[op.module_key] = (op.module_name, op.module_description)

            module_list = [
                module_meta[module_key]
                for module_key in sorted(module_groups, key=lambda key: module_meta[key][0].lower())
            ]
            expected[tag_topic_file] = render_modules_index_topic(f"Tag_{tag_slug}", module_list)

            for module_key in sorted(module_groups, key=lambda key: module_meta[key][0].lower()):
                module_name, module_description = module_meta[module_key]
                module_slug = safe_token(module_name)
                module_topic_file = generated_dir / f"modules_module_{module_slug}.topic"
                expected[module_topic_file] = render_module_topic(
                    module_name=module_name,
                    module_description=module_description,
                    topic_id=f"modules_module_{module_slug}",
                )

                module_ops = sorted(module_groups[module_key], key=lambda op: (op.module_name.lower(), op.title.lower(), op.path, op.method))
                total_pages = max(1, (len(module_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
                for page_index in range(total_pages):
                    page_number = page_index + 1
                    page_topic_file = generated_dir / f"modules_module_{module_slug}_page_{page_number}.topic"
                    expected[page_topic_file] = render_tag_page_topic(
                        tag=module_name,
                        topic_id=f"modules_module_{module_slug}_page_{page_number}",
                        page_number=page_number,
                        total_pages=total_pages,
                    )

                for op in module_ops:
                    expected[generated_dir / op.topic_file] = render_operation_topic(op)
            continue

        if tag == "Config":
            config_groups: Dict[str, List[Operation]] = {}
            config_meta: Dict[str, Tuple[str, str]] = {}
            for op in tag_ops:
                config_groups.setdefault(op.config_key, []).append(op)
                config_meta[op.config_key] = (op.config_name, op.config_description)

            config_list = [
                config_meta[config_key]
                for config_key in sorted(config_groups, key=lambda key: config_meta[key][0].lower())
            ]
            expected[tag_topic_file] = render_config_index_topic(f"Tag_{tag_slug}", config_list)

            for config_key in sorted(config_groups, key=lambda key: config_meta[key][0].lower()):
                config_name, config_description = config_meta[config_key]
                config_slug = safe_token(config_name)
                config_topic_file = generated_dir / f"config_module_{config_slug}.topic"
                expected[config_topic_file] = render_module_topic(
                    module_name=config_name,
                    module_description=config_description,
                    topic_id=f"config_module_{config_slug}",
                )

                config_ops = sorted(config_groups[config_key], key=lambda op: (op.title.lower(), op.path, op.method))
                total_pages = max(1, (len(config_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
                for page_index in range(total_pages):
                    page_number = page_index + 1
                    page_topic_file = generated_dir / f"config_module_{config_slug}_page_{page_number}.topic"
                    expected[page_topic_file] = render_tag_page_topic(
                        tag=config_name,
                        topic_id=f"config_module_{config_slug}_page_{page_number}",
                        page_number=page_number,
                        total_pages=total_pages,
                    )

                for op in config_ops:
                    expected[generated_dir / op.topic_file] = render_operation_topic(op)
            continue

        expected[tag_topic_file] = render_tag_topic(tag, f"Tag_{tag_slug}")

        total_pages = max(1, (len(tag_ops) + OPS_PER_PAGE - 1) // OPS_PER_PAGE)
        for page_index in range(total_pages):
            page_number = page_index + 1
            page_topic_file = generated_dir / f"Tag_{tag_slug}_Page_{page_number}.topic"
            expected[page_topic_file] = render_tag_page_topic(
                tag=tag,
                topic_id=f"Tag_{tag_slug}_Page_{page_number}",
                page_number=page_number,
                total_pages=total_pages,
            )

        for op in tag_ops:
            expected[generated_dir / op.topic_file] = render_operation_topic(op)

    operation_topic_names = {f"{op.topic_id}.topic" for op in operations}
    operation_topics = [
        path for path in expected
        if path.parent == generated_dir and path.name in operation_topic_names
    ]
    if len(operation_topics) != len(operations):
        raise ValueError(
            f"Coverage mismatch: expected {len(operations)} operation topics, built {len(operation_topics)} files."
        )

    return expected


def generate(root: Path) -> None:
    expected = build_expected_outputs(root)
    generated_dir = root / "documentation" / "topics" / "generated"
    generated_dir.mkdir(parents=True, exist_ok=True)

    expected_generated_paths = {
        path for path in expected if path.parent == generated_dir and path.suffix == ".topic"
    }
    for existing_file in generated_dir.glob("*.topic"):
        if existing_file not in expected_generated_paths:
            existing_file.unlink()

    changed_files = 0
    for path, content in expected.items():
        if write_if_changed(path, content):
            changed_files += 1

    print(f"Generated documentation artifacts. Updated files: {changed_files}")


def check(root: Path) -> None:
    expected = build_expected_outputs(root)
    generated_dir = root / "documentation" / "topics" / "generated"

    failures: List[str] = []
    for path, content in expected.items():
        if not path.exists():
            failures.append(f"Missing file: {path}")
            continue
        current = path.read_text(encoding="utf-8")
        if current != content:
            failures.append(f"Outdated file: {path}")

    if generated_dir.exists():
        expected_generated_paths = {
            path for path in expected if path.parent == generated_dir and path.suffix == ".topic"
        }
        for existing_file in generated_dir.glob("*.topic"):
            if existing_file not in expected_generated_paths:
                failures.append(f"Stale generated topic: {existing_file}")

    if failures:
        print("Documentation check failed:")
        for failure in failures:
            print(f"- {failure}")
        print("Run: python scripts/generate_writerside_openapi_docs.py generate")
        raise SystemExit(1)

    print("Documentation check passed.")


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate Writerside API docs from openapi.yaml.")
    parser.add_argument("command", choices=["generate", "check"], help="Generation mode")
    args = parser.parse_args()

    root = Path(__file__).resolve().parents[1]
    try:
        if args.command == "generate":
            generate(root)
        else:
            check(root)
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        raise SystemExit(1)


if __name__ == "__main__":
    main()
