#!/usr/bin/env python3
"""Doxygen filter for shell-like files.

The filter converts shell scripts to a C-like stream so Doxygen can parse
function declarations reliably while preserving function documentation from
preceding comment blocks.
"""

from __future__ import annotations

import hashlib
import re
import sys
from pathlib import Path

FUNC_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*\{\s*$")
FUNC_KEYWORD_RE = re.compile(
    r"^\s*function\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(\s*\))?\s*(?:\{|$)"
)
SEPARATOR_RE = re.compile(r"^[#\-=_*\s]+$")
PARAM_TAG_RE = re.compile(r"^@param(?:\[[^\]]+\])?\s+([A-Za-z0-9_]+)\s*(.*)$", re.IGNORECASE)
ARG_TAG_RE = re.compile(r"^arg([0-9]+)\s*:\s*(.+)$", re.IGNORECASE)


def has_explicit_doc_tags(block: list[str]) -> bool:
    for line in block:
        low = line.strip().lower()
        if low.startswith("@brief") or low.startswith("@details"):
            return True
        if PARAM_TAG_RE.match(line) or ARG_TAG_RE.match(line):
            return True
    return False


def strip_comment_prefix(line: str) -> str:
    return re.sub(r"^\s*#\s?", "", line.rstrip("\n"))


def extract_header_comment(lines: list[str]) -> list[str]:
    header: list[str] = []
    started = False
    for raw in lines:
        if raw.startswith("#!") and not started:
            continue
        if re.match(r"^\s*#", raw):
            started = True
            header.append(strip_comment_prefix(raw))
            continue
        if raw.strip() == "" and started:
            header.append("")
            continue
        if started:
            break
    return header


def clean_comment_lines(lines: list[str]) -> list[str]:
    out: list[str] = []
    prev_blank = True
    for line in lines:
        text = line.strip()
        if text == "" or SEPARATOR_RE.match(text):
            if not prev_blank:
                out.append("")
                prev_blank = True
            continue
        out.append(text)
        prev_blank = False
    while out and out[-1] == "":
        out.pop()
    return out


def extract_preceding_comment(lines: list[str], idx: int) -> list[str]:
    block: list[str] = []
    seen_comment = False
    i = idx - 1
    while i >= 0:
        raw = lines[i]
        if re.match(r"^\s*#", raw):
            block.append(strip_comment_prefix(raw))
            seen_comment = True
            i -= 1
            continue
        if raw.strip() == "" and seen_comment:
            block.append("")
            i -= 1
            continue
        break
    block.reverse()
    return clean_comment_lines(block)


def parse_doc_block(block: list[str], fallback: str) -> tuple[str, list[str], list[tuple[str, str]]]:
    brief = ""
    details: list[str] = []
    params: list[tuple[str, str]] = []
    explicit_tags = has_explicit_doc_tags(block)
    context = ""

    for line in block:
        stripped = line.strip()
        low = stripped.lower()

        if stripped == "":
            context = ""
            continue

        if low.startswith("@brief"):
            brief = stripped[6:].strip()
            context = "brief"
            continue
        if low.startswith("@details"):
            detail = stripped[8:].strip()
            if detail:
                details.append(detail)
            context = "details"
            continue

        param_match = PARAM_TAG_RE.match(stripped)
        if param_match:
            pname = param_match.group(1)
            pdesc = param_match.group(2).strip() or "Function argument."
            params.append((pname, pdesc))
            context = ""
            continue

        arg_match = ARG_TAG_RE.match(stripped)
        if arg_match:
            pname = f"arg{arg_match.group(1)}"
            params.append((pname, arg_match.group(2).strip()))
            context = ""
            continue

        if explicit_tags:
            if context in ("brief", "details"):
                details.append(stripped)
            continue

        if not brief:
            brief = stripped
        else:
            details.append(stripped)

    if not brief:
        brief = fallback

    return brief, details, params


def emit_file_block(path: Path, header_lines: list[str]) -> None:
    cleaned = clean_comment_lines(header_lines)
    fallback = f"Shell workflow resource: {path.name}."
    brief, details, _ = parse_doc_block(cleaned, fallback)

    print("/**")
    print(f" * @file {path.name}")
    print(f" * @brief {brief}")
    if details:
        print(" * @details")
        for line in details:
            print(f" * {line}")
    print(" */")
    print()


def detect_functions(lines: list[str]) -> list[tuple[str, int]]:
    found: list[tuple[str, int]] = []
    seen: set[str] = set()
    for idx, line in enumerate(lines):
        m = FUNC_RE.match(line)
        if m:
            name = m.group(1)
        else:
            m2 = FUNC_KEYWORD_RE.match(line)
            if not m2:
                continue
            name = m2.group(1)

        if name in seen:
            continue
        seen.add(name)
        found.append((name, idx))
    return found


def emit_function_block(name: str, comment_lines: list[str]) -> None:
    fallback = f"Workflow helper function {name}."
    brief, details, params = parse_doc_block(comment_lines, fallback)

    print("/**")
    print(f" * @brief {brief}")
    if details:
        print(" * @details")
        for line in details:
            print(f" * {line}")
    for pname, pdesc in params:
        text = pdesc if pdesc.endswith((".", "!", "?")) else f"{pdesc}."
        print(f" * @param[in] {pname} {text}")
    print(" */")
    if params:
        args = ", ".join(f"const char *{pname}" for pname, _ in params)
    else:
        args = "void"
    print(f"void {name}({args});")
    print()


def main() -> int:
    if len(sys.argv) != 2:
        return 1

    path = Path(sys.argv[1])
    try:
        lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
    except OSError:
        return 1

    emit_file_block(path, extract_header_comment(lines))

    functions = detect_functions(lines)
    for name, idx in functions:
        emit_function_block(name, extract_preceding_comment(lines, idx))

    if not functions:
        digest = hashlib.md5(str(path).encode("utf-8")).hexdigest()[:10]
        print(f"typedef int doxygen_shell_file_{digest};")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
