"""
Generates a complete structural catalog of every table in the legacy Access
source files (including excluded/scratch tables, for documentation
completeness) and writes it as JSON.

This is the source data for docs/LEGACY_DATABASES.md. Re-run this any time
the source .accdb/.mdb files change, then regenerate the doc from the JSON.

Must run on a Windows machine with the Microsoft Access Database Engine
ODBC driver installed (pyodbc can't reach Access files otherwise) — see
docs/LEGACY_DATABASES.md for why non-Windows machines can't do this step
themselves and have to work from this catalog / the staged Parquet output
instead.

Usage:
    python catalog_schema.py --output catalog.json
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import extract
from config import SOURCES


def get_access_type_labels(cnxn, table_name: str) -> dict[str, str] | None:
    """Best-effort: cursor.columns() gives real Access type names/sizes
    (VARCHAR(255), DOUBLE, LONGCHAR, LONGBINARY, COUNTER, BIT, CURRENCY...)
    but decode-errors on a subset of tables (see extract.py). Returns None
    on failure so the caller can fall back to pandas-inferred dtypes."""
    try:
        cursor = cnxn.cursor()
        labels = {}
        for col in cursor.columns(table=table_name):
            size = col.column_size
            labels[col.column_name] = f"{col.type_name}({size})" if size else col.type_name
        return labels
    except Exception:
        return None


def catalog_source(source_name: str, source_cfg: dict) -> dict:
    path = source_cfg["path"]
    result = {"path": str(path), "tables": {}}

    if not Path(path).exists():
        result["error"] = "file not found"
        return result

    cnxn = extract.connect(path)
    tables = extract.list_tables(cnxn)

    for table_name in tables:
        entry = {"excluded": table_name in source_cfg["exclude"]}
        access_types = get_access_type_labels(cnxn, table_name)

        try:
            df = extract.read_table(cnxn, table_name)
        except Exception as exc:  # noqa: BLE001
            entry["error"] = str(exc)
            result["tables"][table_name] = entry
            continue

        original_columns = df.attrs.get("original_columns", list(df.columns))
        columns = []
        for orig_name, sanitized_name in zip(original_columns, df.columns):
            dtype = str(df[sanitized_name].dtype)
            has_nulls = bool(df[sanitized_name].isna().any()) if len(df) else None
            type_label = None
            if access_types is not None:
                type_label = access_types.get(orig_name)
            columns.append(
                {
                    "name": orig_name,
                    "access_type": type_label,
                    "pandas_dtype": dtype,
                    "nullable": has_nulls,
                }
            )

        entry["row_count"] = len(df)
        entry["column_count"] = len(columns)
        entry["columns"] = columns
        entry["access_type_metadata_available"] = access_types is not None
        result["tables"][table_name] = entry

    return result


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=Path("catalog.json"))
    args = parser.parse_args()

    catalog = {}
    for source_name, source_cfg in SOURCES.items():
        print(f"Cataloging {source_name}...")
        catalog[source_name] = catalog_source(source_name, source_cfg)

    args.output.write_text(json.dumps(catalog, indent=2, default=str), encoding="utf-8")
    print(f"Wrote {args.output}")


if __name__ == "__main__":
    main()
