"""Generate Python field-name constants from CEP JSON Schemas.

This is a companion to `rust_generated.py` that keeps all camelCase
JSON field names in one generated place, so adapters and other Python
code never hand-type schema field names.

It currently generates two modules:

- civic_interconnect.cep.constants.entity_fields
- civic_interconnect.cep.constants.record_envelope_fields

File: src/python/src/civic_interconnect/cep/codegen/python_constants.py
"""

from collections.abc import Iterable
from dataclasses import dataclass
import json
from pathlib import Path

# Default locations within the civic-interconnect repo
DEFAULT_ENTITY_SCHEMA = Path("schemas/core/cep.entity.schema.json")
DEFAULT_ENVELOPE_SCHEMA = Path("schemas/core/cep.record-envelope.schema.json")

DEFAULT_ENTITY_CONSTANTS_OUT = Path(
    "src/python/src/civic_interconnect/cep/constants/entity_fields.py"
)
DEFAULT_ENVELOPE_CONSTANTS_OUT = Path(
    "src/python/src/civic_interconnect/cep/constants/record_envelope_fields.py"
)


@dataclass(frozen=True)
class FieldConstant:
    """Represents a single Python constant mapping to a JSON field name."""

    const_name: str  # e.g. ENTITY_LEGAL_NAME
    json_name: str  # e.g. "legalName"


def _to_upper_snake(name: str) -> str:
    """Convert a camelCase / mixedCase / lower case string to UPPER_SNAKE_CASE.

    This is intentionally conservative: it handles typical
    camelCase / lowerCamelCase CEP field names.
    """
    chars: list[str] = []
    for ch in name:
        if ch.isupper():
            # Separate word boundary with underscore
            if chars and chars[-1] != "_":
                chars.append("_")
            chars.append(ch)
        elif ch == "-":
            chars.append("_")
        else:
            chars.append(ch.upper())
    return "".join(chars)


# ---------------------------------------------------------------------------
# Entity schema constants
# ---------------------------------------------------------------------------


def _collect_entity_properties(entity_schema: dict) -> set[str]:
    """Collect JSON property names relevant for the EntityRecord.

    The entity schema uses `allOf` to compose the core record envelope
    plus domain-specific fields. For now, we collect all properties
    that appear in those blocks and let higher layers decide which
    ones to use.
    """
    properties: set[str] = set()

    # Top-level properties, if any
    top_props = entity_schema.get("properties")
    if isinstance(top_props, dict):
        properties.update(top_props.keys())

    # Properties inside allOf clauses
    for part in entity_schema.get("allOf", []):
        sub_props = part.get("properties")
        if isinstance(sub_props, dict):
            properties.update(sub_props.keys())

    return properties


def _build_entity_field_constants(property_names: Iterable[str]) -> list[FieldConstant]:
    """Create Python constant definitions for entity properties.

    We prefix everything with ENTITY_ to avoid collisions and make the
    usage self-documenting in code.
    """
    constants: list[FieldConstant] = []

    for json_name in sorted(property_names):
        # We skip recordKind here; it is envelope-level and const "entity".
        if json_name == "recordKind":
            continue

        const_base = _to_upper_snake(json_name)
        const_name = f"ENTITY_{const_base}"
        constants.append(FieldConstant(const_name=const_name, json_name=json_name))

    return constants


def write_entity_constants(entity_schema_path: Path, out_path: Path) -> None:
    """Generate the Python constants module for entity field names.

    - entity_schema_path: path to `cep.entity.schema.json`
    - out_path: path to write `entity_fields.py`
    """
    with entity_schema_path.open("r", encoding="utf-8") as f:
        schema = json.load(f)

    prop_names = _collect_entity_properties(schema)
    constants = _build_entity_field_constants(prop_names)

    out_path.parent.mkdir(parents=True, exist_ok=True)

    with out_path.open("w", encoding="utf-8") as f:
        f.write(
            "# This file is AUTO-GENERATED by cx codegen-python-constants.\n"
            "# Do not edit by hand. To change field names, update the JSON\n"
            "# schema (schemas/cep.entity.schema.json) and re-run:\n"
            "#\n"
            "#     uv run cx codegen-python-constants\n"
            "#\n"
            '"""Python constants for CEP Entity JSON field names.\n'
            "\n"
            "These constants ensure adapters never hand-type camelCase\n"
            "field names. All values MUST match the JSON Schemas.\n"
            '"""\n\n'
        )

        for const in constants:
            f.write(f'{const.const_name} = "{const.json_name}"\n')


# ---------------------------------------------------------------------------
# Record envelope schema constants
# ---------------------------------------------------------------------------


def _build_envelope_field_constants(envelope_schema: dict) -> list[FieldConstant]:
    """Create Python constant definitions for record envelope and nested fields."""
    constants: list[FieldConstant] = []

    # Helper to add constants for a group of properties
    def add_props(prefix: str, props: dict | None):
        if not isinstance(props, dict):
            return
        for json_name in sorted(props):
            const_name = f"{prefix}_{_to_upper_snake(json_name)}"
            constants.append(FieldConstant(const_name=const_name, json_name=json_name))

    # Top-level envelope properties
    add_props("ENVELOPE", envelope_schema.get("properties"))

    # Nested groups inside $defs
    defs = (
        envelope_schema.get("$defs", {}) if isinstance(envelope_schema.get("$defs"), dict) else {}
    )

    add_props("STATUS", defs.get("status", {}).get("properties"))
    add_props("TIMESTAMPS", defs.get("timestamps", {}).get("properties"))
    add_props("ATTESTATION", defs.get("attestation", {}).get("properties"))

    return constants


def write_envelope_constants(envelope_schema_path: Path, out_path: Path) -> None:
    """Generate the Python constants module for record envelope field names.

    - envelope_schema_path: path to `cep.record-envelope.schema.json`
    - out_path: path to write `record_envelope_fields.py`
    """
    with envelope_schema_path.open("r", encoding="utf-8") as f:
        schema = json.load(f)

    constants = _build_envelope_field_constants(schema)

    out_path.parent.mkdir(parents=True, exist_ok=True)

    with out_path.open("w", encoding="utf-8") as f:
        f.write(
            "# This file is AUTO-GENERATED by cx codegen-python-constants.\n"
            "# Do not edit by hand. To change field names, update the JSON\n"
            "# schemas (schemas/cep.record-envelope.schema.json) and re-run:\n"
            "#\n"
            "#     uv run cx codegen-python-constants\n"
            "#\n"
            '"""Python constants for CEP record envelope JSON field names.\n'
            "\n"
            "These constants ensure adapters and tools never hand-type\n"
            "camelCase field names for the shared envelope, status,\n"
            "timestamps, and attestations.\n"
            '"""\n\n'
        )

        for const in constants:
            f.write(f'{const.const_name} = "{const.json_name}"\n')


def write_all_constants(
    entity_schema_path: Path = DEFAULT_ENTITY_SCHEMA,
    entity_out: Path = DEFAULT_ENTITY_CONSTANTS_OUT,
    envelope_schema_path: Path = DEFAULT_ENVELOPE_SCHEMA,
    envelope_out: Path = DEFAULT_ENVELOPE_CONSTANTS_OUT,
) -> None:
    """Generate both entity and record envelope constants in one call."""
    write_entity_constants(entity_schema_path, entity_out)
    write_envelope_constants(envelope_schema_path, envelope_out)


if __name__ == "__main__":
    # Allow running as a standalone script during development:
    write_all_constants()
    print(f"Wrote {DEFAULT_ENTITY_CONSTANTS_OUT}")
    print(f"Wrote {DEFAULT_ENVELOPE_CONSTANTS_OUT}")
