"""
Seed the label printers for one tenant, from backend/printer/*.json.

    python seed_printers.py [tenant-slug]
    # tenant-slug default: SEED_TENANT_SLUG, then demo-enterprise

One JSON file per printer, named after the printer. Adding a printer to a
deployment is dropping a file in backend/printer/ and running this again — no
Python to edit, no rebuild, and the file is reviewable in a diff.

Idempotent: matches on (tenant, name) and UPDATES an existing row rather than
inserting a duplicate. Safe to run on every deploy.

WHY THIS IS SEPARATE from the roles/users seeder: printers are physical. Which
machine is on the network, what roll is loaded in it and whether it prints QR or
Code128 are site facts, not application data, and they change without the code
changing. Re-running the persona seeder resets 22 passwords; re-running this one
touches nothing but the printer list.

Nothing here has to be right at deploy time. The registry is a convenience — the
app falls back to the .env single-printer config when the table is empty
(ams/services/print_agent.py::resolve_config), and every value below is editable
in the UI under Tag Management -> Manage printers.
"""
import asyncio
import json
import os
import sys
import uuid
from pathlib import Path

from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from ams.core.config import settings

PRINTER_DIR = Path(__file__).parent / "printer"

# The columns the printers table actually accepts (alembic 0134/0135). Anything
# else in a JSON file — "notes", a typo — is reported and ignored rather than
# silently dropped or crashing the run.
FIELDS = ("name", "model", "ip", "subnet", "label_size", "code_type", "is_default", "is_active")
REQUIRED = ("name", "model", "label_size", "code_type")
VALID_CODE_TYPES = ("qr", "barcode")


def _load() -> list[dict]:
    if not PRINTER_DIR.is_dir():
        raise SystemExit(f"No printer directory at {PRINTER_DIR}")
    printers, problems = [], []
    for path in sorted(PRINTER_DIR.glob("*.json")):
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except json.JSONDecodeError as e:
            problems.append(f"{path.name}: not valid JSON ({e})")
            continue
        missing = [k for k in REQUIRED if not data.get(k)]
        if missing:
            problems.append(f"{path.name}: missing {', '.join(missing)}")
            continue
        if data["code_type"] not in VALID_CODE_TYPES:
            problems.append(f"{path.name}: code_type must be one of {VALID_CODE_TYPES}")
            continue
        unknown = sorted(set(data) - set(FIELDS) - {"notes"})
        if unknown:
            print(f"  note: {path.name} has fields this table does not have, ignored: {', '.join(unknown)}")
        printers.append({k: data[k] for k in FIELDS if k in data} | {"_file": path.name})

    if problems:
        print("  ABORTING — printer files are not usable:")
        for p in problems:
            print(f"    {p}")
        print("  Nothing was changed.")
        raise SystemExit(1)

    # The table has a unique index allowing ONE default per tenant, so two files
    # both claiming is_default would fail mid-run with a constraint error and
    # leave the list half-written. Catch it here, by name, before writing.
    defaults = [p["name"] for p in printers if p.get("is_default")]
    if len(defaults) > 1:
        raise SystemExit(
            f"  ABORTING — more than one printer is marked is_default: {', '.join(defaults)}.\n"
            f"  The database allows only one default per tenant. Nothing was changed."
        )
    return printers


def _database_url_for(tenant_slug: str) -> str:
    """Same alias resolution as the persona seeder, so seeding and serving can
    never disagree about which database a tenant lives in."""
    aliases = {"shared": settings.DATABASE_URL, "government-dedicated": settings.GOVERNMENT_DB_URL}
    aliases.update(json.loads(os.environ.get("EXTRA_DB_ALIASES", "{}") or "{}"))
    mapping = json.loads(os.environ.get("TENANT_DB_MAP", "{}") or "{}")
    alias = mapping.get(tenant_slug, "shared")
    if alias not in aliases:
        raise SystemExit(
            f"Tenant {tenant_slug!r} maps to database alias {alias!r}, which is not defined.\n"
            f"Known aliases: {', '.join(sorted(aliases))}. Add it to EXTRA_DB_ALIASES."
        )
    return aliases[alias]


async def seed(session: AsyncSession, tenant_slug: str, printers: list[dict]) -> None:
    row = (await session.execute(
        text("SELECT id FROM public.tenants WHERE slug = :s"), {"s": tenant_slug}
    )).fetchone()
    if not row:
        # Exit non-zero: this used to return 0, so a caller printed
        # "printers seeded" after seeding nothing at all.
        raise SystemExit(
            f"  Tenant {tenant_slug!r} not found in public.tenants — nothing seeded.\n"
            f"  Pass the slug explicitly: python seed_printers.py <tenant-slug>"
        )
    tenant_id = row.id
    schema = f"tenant_{str(tenant_id).replace('-', '_')}"
    await session.execute(text(f"SET search_path TO {schema}, public"))

    # Clearing the default first means a file that moves is_default from printer
    # A to printer B does not trip the one-default-per-tenant unique index
    # halfway through the loop.
    if any(p.get("is_default") for p in printers):
        await session.execute(text("UPDATE printers SET is_default = false WHERE tenant_id = :t"),
                              {"t": tenant_id})

    for p in printers:
        name = p["name"]
        existing = (await session.execute(
            text("SELECT id FROM printers WHERE tenant_id = :t AND name = :n"),
            {"t": tenant_id, "n": name},
        )).fetchone()
        values = {
            "t": tenant_id,
            "n": name,
            "model": p.get("model", "QL-820NWB"),
            "ip": p.get("ip") or None,
            "subnet": p.get("subnet") or None,
            "label_size": p.get("label_size", "62"),
            "code_type": p.get("code_type", "qr"),
            "is_default": bool(p.get("is_default", False)),
            "is_active": bool(p.get("is_active", True)),
        }
        if existing:
            await session.execute(text("""
                UPDATE printers SET model=:model, ip=:ip, subnet=:subnet,
                       label_size=:label_size, code_type=:code_type,
                       is_default=:is_default, is_active=:is_active, updated_at=now()
                 WHERE id=:id"""), values | {"id": existing.id})
            print(f"  updated: {name}  ({values['label_size']}, {values['code_type']})")
        else:
            await session.execute(text("""
                INSERT INTO printers (id, tenant_id, name, model, ip, subnet, label_size,
                                      code_type, is_default, is_active, created_at, updated_at)
                VALUES (:id, :t, :n, :model, :ip, :subnet, :label_size,
                        :code_type, :is_default, :is_active, now(), now())"""),
                values | {"id": uuid.uuid4()})
            print(f"  created: {name}  ({values['label_size']}, {values['code_type']})")

    await session.commit()


async def main() -> None:
    tenant_slug = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SEED_TENANT_SLUG", "demo-enterprise")
    printers = _load()
    db_url = _database_url_for(tenant_slug)
    print(f"Seeding {len(printers)} printer(s) into tenant '{tenant_slug}' on {db_url.split('@')[-1]}")

    engine = create_async_engine(db_url, echo=False)
    Session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    try:
        async with Session() as session:
            await seed(session, tenant_slug, printers)
    finally:
        await engine.dispose()
    print("Done. Change any of it in the app: Tag Management -> Manage printers.")


if __name__ == "__main__":
    asyncio.run(main())
