"""Lightweight makeindex4-compatible wrapper."""

from __future__ import annotations

import argparse
from collections.abc import Iterable, Sequence
from pathlib import Path
import sys
import tempfile
import traceback

from xindy.dsl.interpreter import StyleInterpreter
from xindy.index import build_index_entries
from xindy.markup import render_index
from xindy.raw.reader import RawIndexEntry

from .tex2xindy import convert_idx_to_raw_entries, parse_idx, write_raw


def _compress_key_parts(entry: RawIndexEntry) -> RawIndexEntry:
    key = tuple(" ".join(part.split()) for part in entry.key)
    return RawIndexEntry(key=key, locref=entry.locref, attr=entry.attr, extras=entry.extras)


def _build_temp_style(
    attrs: Iterable[str],
    dest: Path,
    *,
    ignore_blanks: bool = False,
    crossref_attrs: Iterable[str] = (),
) -> Path:
    attr_list = list(dict.fromkeys(a for a in attrs if a))
    # ensure deterministic order
    attr_list.sort()
    crossref_list = list(dict.fromkeys(a for a in crossref_attrs if a))
    crossref_list.sort()
    lines: list[str] = [
        ";; Temporary style generated by makeindex4 wrapper",
        "(define-attributes ((" + " ".join(f'"{a}"' for a in attr_list) + ' "default")))',
    ]
    if ignore_blanks:
        lines.append("(define-sort-rule-orientations (forward))")
        lines.append('(sort-rule " " "" :run 0)')
    for attr in attr_list:
        lines.append(f'(markup-locref :attr "{attr}" :open "\\\\{attr}{{" :close "}}")')
    for cref in crossref_list or ["see"]:
        lines.append(f'(define-crossref-class "{cref}")')
        lines.append(f'(markup-crossref-list :open "\\\\{cref}{{" :close "}}{{}}" :class "{cref}")')
    order = [
        '"roman-page-numbers"',
        '"arabic-page-numbers"',
        '"alpha-page-numbers"',
        '"Roman-page-numbers"',
        '"Alpha-page-numbers"',
        '"see"',
        *[f'"{cref}"' for cref in crossref_list],
    ]
    order_expr = " ".join(order)
    lines.extend(
        [
            '(define-location-class "arabic-page-numbers" ("arabic-numbers"))',
            '(define-location-class "roman-page-numbers"  ("roman-numbers-lowercase"))',
            '(define-location-class "Roman-page-numbers"  ("roman-numbers-uppercase"))',
            '(define-location-class "alpha-page-numbers"  ("alpha"))',
            '(define-location-class "Alpha-page-numbers"  ("ALPHA"))',
            f"(define-location-class-order ({order_expr}))",
            '(require "tex/makeidx4.xdy")',
        ]
    )
    dest.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return dest


class _Logger:
    """Minimal logger that mirrors to stderr when requested and writes .ilg output."""

    def __init__(self, path: Path):
        self.path = path
        self._lines: list[str] = []
        self._mirror = path == Path("-")

    def info(self, message: str, *, mirror: bool | None = None) -> None:
        self._write(message, mirror=mirror)

    def warn(self, message: str, *, mirror: bool | None = None) -> None:
        self._write(f"warning: {message}", mirror=mirror)

    def error(self, message: str, *, mirror: bool | None = None) -> None:
        self._write(f"error: {message}", mirror=mirror)

    def _write(self, message: str, *, mirror: bool | None = None) -> None:
        self._lines.append(message)
        if mirror is None:
            mirror = self._mirror
        if mirror:
            sys.stderr.write(message + "\n")

    def flush(self) -> None:
        if self.path == Path("-"):
            return
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(
            "\n".join(self._lines) + ("\n" if self._lines else ""), encoding="utf-8"
        )


def _format_error(exc: Exception) -> str:
    if isinstance(exc, FileNotFoundError):
        filename = getattr(exc, "filename", None) or getattr(exc, "filename2", None)
        if filename:
            return f"file not found: {filename}"
        return str(exc)
    if isinstance(exc, UnicodeDecodeError):
        return (
            f"could not decode input with encoding '{exc.encoding}' "
            f"(invalid bytes at position {exc.start}-{exc.end})"
        )
    return str(exc)


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        prog="makeindex4",
        description="makeindex-compatible wrapper around xindy (Python port).",
    )
    parser.add_argument("idx", help="Input .idx file")
    parser.add_argument("-o", "--output", help="Output .ind file (default: basename.idx -> .ind)")
    parser.add_argument("-t", "--log", help="Log file (default: basename.ilg)")
    parser.add_argument(
        "-c", action="store_true", help="Compress whitespace in keys (makeindex -c)"
    )
    parser.add_argument("-l", action="store_true", help="Ignore blanks in sorting (makeindex -l)")
    parser.add_argument("--input-encoding", default="latin-1", help="Encoding of .idx input")
    parser.add_argument("--output-encoding", default="utf-8", help="Encoding of .ind output")
    parser.add_argument(
        "-g", action="store_true", help="(makeindex -g) not supported, emits warning"
    )
    parser.add_argument(
        "-q", action="store_true", help="(makeindex -q) not supported, emits warning"
    )
    parser.add_argument(
        "-r", action="store_true", help="(makeindex -r) not supported, emits warning"
    )
    parser.add_argument("-p", help="(makeindex -p) not supported, emits warning")
    parser.add_argument("-s", help="(makeindex -s) not supported, emits warning")
    parser.add_argument("--debug", action="store_true", help="Show tracebacks on errors.")
    args = parser.parse_args(argv)

    idx_path = Path(args.idx) if args.idx != "-" else None
    base = idx_path.with_suffix("") if idx_path else Path("stdin")
    out_path = (
        Path(args.output) if args.output else (base.with_suffix(".ind") if idx_path else Path("-"))
    )
    log_path = Path(args.log) if args.log else (base.with_suffix(".ilg") if idx_path else Path("-"))
    logger = _Logger(log_path)

    try:
        if idx_path is None:
            idx_text = sys.stdin.buffer.read().decode(args.input_encoding)
            entries = [e.to_raw() for e in parse_idx(idx_text)]
        else:
            entries = convert_idx_to_raw_entries(idx_path, encoding=args.input_encoding)
        if args.c:
            entries = [_compress_key_parts(e) for e in entries]
        attrs = {e.attr for e in entries if e.attr}

        crossref_attrs = {e.attr for e in entries if e.attr and e.extras.get("xref")}

        with tempfile.TemporaryDirectory() as tmpdir:
            style_path = _build_temp_style(
                attrs,
                Path(tmpdir) / "makeindex4.xdy",
                ignore_blanks=args.l,
                crossref_attrs=crossref_attrs,
            )
            raw_path = Path(tmpdir) / "tmp.raw"
            write_raw(entries, raw_path, encoding=args.output_encoding)

            state = StyleInterpreter().load(style_path)
            index = build_index_entries(entries, state)
            output = render_index(index, style_state=state)
            if out_path == Path("-"):
                sys.stdout.write(output)
            else:
                out_path.write_text(output, encoding=args.output_encoding)
            logger.info(f"Processed {len(entries)} entries")
    except Exception as exc:  # pragma: no cover - defensive path
        if args.debug:
            traceback.print_exc()
        friendly = _format_error(exc)
        print(f"makeindex4 error: {friendly}", file=sys.stderr)
        logger.error(friendly, mirror=False)
        logger.flush()
        return 1

    for flag, name in [
        (args.g, "-g"),
        (args.q, "-q"),
        (args.r, "-r"),
        (bool(args.p), "-p"),
        (bool(args.s), "-s"),
    ]:
        if flag:
            message = f"makeindex option {name} not supported by Python wrapper"
            if log_path != Path("-"):
                print(f"Warning: {message}", file=sys.stderr)
            logger.warn(message)
    logger.flush()
    return 0


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