"""CLI subcommand: ``python -m cyclone seed``.

Populates the local DB with a deterministic batch of sample claims
plus matching activity events, so the Dashboard / Claims / Activity
Log pages have something to render in a fresh dev environment.

This is a dev-only convenience — the production DB never sees this
command (no ``[env: dev]`` gate, but it's never wired into any
container image). It writes rows with a recognizable batch id prefix
(``SEED-``) so ``--reset`` can clean them up without touching real
ingested data.

Why the data shape is hand-rolled here
--------------------------------------

The ``Claim`` ORM table stores its billing_provider / payer /
subscriber / service_lines payloads in a ``raw_json`` blob that the
read path (``store.to_ui_claim_from_orm``) parses back into the UI
shape. Mirroring the 837 parser's structure keeps the UI rendering
identical to a real ingestion — wire format parity, not shortcuts.

Activity rows are similar: ``payload_json`` carries the message
string the Dashboard's activity card shows.

Subcommands
-----------

``python -m cyclone seed``              — insert the default batch.
``python -m cyclone seed --count N``    — insert N claims (default 96).
``python -m cyclone seed --reset``      — wipe previously-seeded rows
                                          first, then insert a fresh batch.
``python -m cyclone seed --status``     — print row counts without
                                          inserting anything.

Exit codes
----------

* 0 — success (or already seeded and not asked to reset).
* 1 — DB error during insert.
* 2 — usage error (bad flag value).

The command is idempotent: re-running without ``--reset`` is a no-op
once a seed batch exists. This keeps ``cyclone``-driven boot scripts
safe to re-run.
"""

from __future__ import annotations

import json
import random
import sys
from datetime import datetime, timedelta, timezone

import click

from cyclone.db import ActivityEvent, Batch, Claim, ClaimState, Remittance, SessionLocal

SEED_BATCH_PREFIX = "SEED-"
SEED_CLAIM_PREFIX = "CLM-S"
SEED_REMIT_PREFIX = "REM-S"
SEED_DEFAULT_COUNT = 96
SEED_ACTIVITY_LIMIT = 28

# Mirror src/data/sampleData.ts on the frontend so the Dashboard looks
# the same in dev mode as it did with the in-memory fixtures.
SAMPLE_PROVIDERS = [
    {
        "npi": "1730187395",
        "name": "Cedar Park Family Medicine",
        "tax_id": "47-3829104",
        "address": "1401 Medical Pkwy",
        "city": "Cedar Park",
        "state": "TX",
        "zip": "78613",
        "phone": "(512) 555-0142",
    },
    {
        "npi": "1528471902",
        "name": "Lakeside Orthopedics",
        "tax_id": "83-1172654",
        "address": "900 W Lake Dr",
        "city": "Austin",
        "state": "TX",
        "zip": "78746",
        "phone": "(512) 555-0188",
    },
    {
        "npi": "1982036471",
        "name": "Hill Country Pediatrics",
        "tax_id": "74-5520183",
        "address": "205 State Hwy 27",
        "city": "Marble Falls",
        "state": "TX",
        "zip": "78654",
        "phone": "(830) 555-0117",
    },
]

SAMPLE_PAYERS = [
    "Blue Cross Blue Shield",
    "United Healthcare",
    "Aetna",
    "Cigna",
    "Humana",
    "Medicare",
    "Medicaid TX",
]

SAMPLE_CPTS = ["99213", "99214", "99203", "93000", "85025", "80053", "73721", "20610"]

SAMPLE_FIRST_NAMES = [
    "Avery", "Jordan", "Riley", "Casey", "Morgan",
    "Quinn", "Reese", "Sasha", "Drew", "Hayden",
]
SAMPLE_LAST_NAMES = [
    "Nguyen", "Patel", "Garcia", "Cohen", "Okafor",
    "Martinez", "Hwang", "Brooks", "Singh", "Tanaka",
]

# Frontend uses "accepted"/"pending" which the backend ClaimState
# enum doesn't carry. Map them to the closest real states so the
# Dashboard's filters (e.g. "status === 'submitted' || 'pending'")
# still find a match for in-flight work.
STATUS_WEIGHTS: list[tuple[ClaimState, int]] = [
    (ClaimState.SUBMITTED, 1),
    (ClaimState.RECEIVED, 1),
    (ClaimState.DENIED, 1),
    (ClaimState.PAID, 3),
    (ClaimState.PAID, 3),
    (ClaimState.PARTIAL, 1),
]

DENIAL_REASONS = [
    "CO-97: Service included in another service",
    "CO-16: Claim lacks information",
    "CO-50: Non-covered service",
    "PR-1: Deductible amount",
]


def _now_utc() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)


def _pick(rng: random.Random, items):
    return items[rng.randint(0, len(items) - 1)]


def _build_seed_rows(
    count: int, *, seed: int = 42,
) -> tuple[Batch, list[Claim], list[ActivityEvent], list[Remittance]]:
    """Build a deterministic Batch + N Claims + matching activity events.

    PAID and PARTIAL claims get a paired ``Remittance`` row (with a
    realistic ``total_paid`` derived from the billed amount) so the
    Dashboard's "Received" KPI lights up; the wire shape mirrors what
    ``CycloneStore.iter_claims`` expects to find via
    ``Claim.matched_remittance_id`` → ``Remittance.total_paid``.

    The seed is fixed so re-running produces identical data — keeps
    screenshots and dev environments stable.
    """
    rng = random.Random(seed)
    now = _now_utc()
    parsed_at = now - timedelta(minutes=5)

    batch = Batch(
        id=f"{SEED_BATCH_PREFIX}{now.strftime('%Y%m%d-%H%M%S')}",
        kind="837",
        input_filename="seed/sample-837.edi",
        parsed_at=parsed_at,
        totals_json={"claim_count": count},
        validation_json={"errors": [], "warnings": []},
        raw_result_json={},
    )

    claims: list[Claim] = []
    activity: list[ActivityEvent] = []
    remittances: list[Remittance] = []
    seq = 10428  # Match the frontend fixture's id range

    for i in range(count):
        provider = _pick(rng, SAMPLE_PROVIDERS)
        payer = _pick(rng, SAMPLE_PAYERS)
        cpt = _pick(rng, SAMPLE_CPTS)
        first = _pick(rng, SAMPLE_FIRST_NAMES)
        last = _pick(rng, SAMPLE_LAST_NAMES)
        state, _ = _pick(rng, STATUS_WEIGHTS)

        days_back = rng.randint(0, 200)
        submitted = now - timedelta(days=days_back, hours=rng.randint(0, 23))

        billed = 80 + rng.randint(0, 1400)
        if state == ClaimState.PAID:
            received = int(billed * (0.6 + rng.random() * 0.4))
        elif state == ClaimState.PARTIAL:
            received = int(billed * (0.2 + rng.random() * 0.3))
        elif state == ClaimState.DENIED:
            received = 0
        else:
            received = 0

        claim_id = f"{SEED_CLAIM_PREFIX}{(seq + i):05d}"
        denial_reason = _pick(rng, DENIAL_REASONS) if state == ClaimState.DENIED else None

        # Build a paired Remittance for any claim with received > 0. Status
        # code 1 = "Primary payer forward" — the 835 CAS code that the
        # remittance mapper turns into "received". Mirror the 835 parser's
        # raw_json shape (a stripped provider/payer/service_lines block)
        # so downstream debug views still render something useful.
        matched_remit_id: str | None = None
        if received > 0:
            matched_remit_id = f"{SEED_REMIT_PREFIX}{(seq + i):05d}"
            remittances.append(Remittance(
                id=matched_remit_id,
                batch_id=batch.id,
                payer_claim_control_number=f"PCN-{(seq + i):05d}",
                claim_id=claim_id,
                status_code="1",
                status_label="Primary payer forward",
                total_charge=float(billed),
                total_paid=float(received),
                adjustment_amount=float(billed - received),
                received_at=submitted + timedelta(days=rng.randint(2, 14)),
                raw_json={
                    "payer": {"name": payer},
                    "provider": {
                        "npi": provider["npi"],
                        "name": provider["name"],
                    },
                    "service_lines": [
                        {
                            "procedure": {"code": cpt},
                            "charge_amount": float(billed),
                            "paid_amount": float(received),
                        }
                    ],
                },
            ))

        raw_json = {
            "billing_provider": {
                "npi": provider["npi"],
                "name": provider["name"],
                "tax_id": provider["tax_id"],
                # 837 parser produces ``address`` as a structured dict so
                # the claim-detail drawer can render line1/line2/city/state/zip.
                # A flat string here crashes ``_address_to_ui`` with
                # ``AttributeError: 'str' object has no attribute 'get'``.
                "address": {
                    "line1": provider["address"],
                    "line2": None,
                    "city": provider["city"],
                    "state": provider["state"],
                    "zip": provider["zip"],
                },
                "phone": provider["phone"],
            },
            "payer": {"name": payer},
            "subscriber": {"first_name": first, "last_name": last},
            "service_lines": [
                {
                    "procedure": {"code": cpt},
                    "charge_amount": float(billed),
                    "service_date": submitted.date().isoformat(),
                }
            ],
        }

        claims.append(Claim(
            id=claim_id,
            batch_id=batch.id,
            patient_control_number=claim_id.replace(SEED_CLAIM_PREFIX, "PCN-"),
            service_date_from=submitted.date(),
            service_date_to=submitted.date(),
            charge_amount=billed,
            provider_npi=provider["npi"],
            payer_id=None,
            state=state,
            state_changed_at=submitted,
            rejection_reason=denial_reason,
            resubmit_count=0,
            matched_remittance_id=matched_remit_id,
            raw_json=raw_json,
        ))

        # First SEED_ACTIVITY_LIMIT claims get an activity event. Mirrors
        # the frontend buildActivity() shape (claim_paid / claim_denied /
        # claim_accepted / claim_submitted) but maps frontend statuses
        # onto the backend's wire enum.
        if i < SEED_ACTIVITY_LIMIT:
            if state == ClaimState.PAID:
                kind = "claim_paid"
                verb = "Paid"
            elif state == ClaimState.DENIED:
                kind = "claim_denied"
                verb = "Denied"
            elif state in (ClaimState.RECEIVED, ClaimState.PARTIAL):
                kind = "claim_accepted"
                verb = "Accepted"
            else:
                kind = "claim_submitted"
                verb = "Submitted"

            payload = {
                "message": f"{verb} {claim_id} · {first} {last}",
                "npi": provider["npi"],
                "amount": float(billed),
            }
            activity.append(ActivityEvent(
                ts=submitted,
                kind=kind,
                batch_id=batch.id,
                claim_id=claim_id,
                remittance_id=None,
                payload_json=payload,
            ))

    return batch, claims, activity, remittances


def _existing_seed_batch_ids(s) -> list[str]:
    rows = s.query(Batch.id).filter(Batch.id.like(f"{SEED_BATCH_PREFIX}%")).all()
    return [r[0] for r in rows]


def _delete_seed_rows(s) -> int:
    """Delete every row that was inserted by the seed.

    The seed writes rows whose ids begin with ``SEED-`` (batches),
    ``CLM-S`` (claims), or ``REM-S`` (remittances). Activity events
    are joined to seeded batches when the batch is still around, but
    we also clean up orphan activity rows (no FK from
    ``activity_events.claim_id`` to ``claims.id``) by ``claim_id``
    prefix. Returns the number of batch rows deleted.
    """
    # Activity first — its rows reference both claims and batches, so
    # delete the events tied to seed claims first, then any that were
    # only tied to a now-orphaned seed batch.
    activity = s.query(ActivityEvent).filter(
        ActivityEvent.claim_id.like(f"{SEED_CLAIM_PREFIX}%")
    ).delete(synchronize_session=False)
    activity2 = s.query(ActivityEvent).filter(
        ActivityEvent.batch_id.like(f"{SEED_BATCH_PREFIX}%")
    ).delete(synchronize_session=False)
    # Remittances (FK to claims; cascade may not fire without FK pragma,
    # so we delete them explicitly to clear the symmetric FK first).
    remits = s.query(Remittance).filter(Remittance.id.like(f"{SEED_REMIT_PREFIX}%")).delete(synchronize_session=False)
    claims = s.query(Claim).filter(Claim.id.like(f"{SEED_CLAIM_PREFIX}%")).delete(synchronize_session=False)
    batches = s.query(Batch).filter(Batch.id.like(f"{SEED_BATCH_PREFIX}%")).delete(synchronize_session=False)
    s.commit()
    click.echo(f"  Removed {batches} seeded batch(es), {claims} claim(s), "
               f"{remits} remittance(s), {activity + activity2} activity event(s).")
    return batches


def _insert(
    batch: Batch,
    claims: list[Claim],
    activity: list[ActivityEvent],
    remittances: list[Remittance],
) -> None:
    with SessionLocal()() as s:
        s.add(batch)
        s.add_all(claims)
        s.add_all(activity)
        s.add_all(remittances)
        s.commit()


def _print_status(s) -> None:
    from sqlalchemy import func
    seed_batches = s.query(func.count(Batch.id)).filter(Batch.id.like(f"{SEED_BATCH_PREFIX}%")).scalar() or 0
    seed_claims = s.query(func.count(Claim.id)).filter(Claim.id.like(f"{SEED_CLAIM_PREFIX}%")).scalar() or 0
    seed_remits = s.query(func.count(Remittance.id)).filter(Remittance.id.like(f"{SEED_REMIT_PREFIX}%")).scalar() or 0
    total_claims = s.query(func.count(Claim.id)).scalar() or 0
    total_remits = s.query(func.count(Remittance.id)).scalar() or 0
    total_activity = s.query(func.count(ActivityEvent.id)).scalar() or 0
    click.echo(f"  Seed batches:  {seed_batches}")
    click.echo(f"  Seed claims:   {seed_claims}")
    click.echo(f"  Seed remits:   {seed_remits}")
    click.echo(f"  Total claims:  {total_claims}")
    click.echo(f"  Total remits:  {total_remits}")
    click.echo(f"  Total activity: {total_activity}")


@click.command("seed")
@click.option(
    "--count",
    default=SEED_DEFAULT_COUNT,
    show_default=True,
    type=click.IntRange(min=1, max=10_000),
    help="Number of claims to insert in the new batch.",
)
@click.option(
    "--reset",
    is_flag=True,
    help="Delete any existing seeded batch (and its claims/activity) before inserting.",
)
@click.option(
    "--status",
    "show_status",
    is_flag=True,
    help="Print current seeded row counts and exit.",
)
def seed_cli(count: int, reset: bool, show_status: bool) -> None:
    """Populate the local DB with a deterministic batch of sample claims."""
    with SessionLocal()() as s:
        if show_status:
            _print_status(s)
            return

        existing = _existing_seed_batch_ids(s)
        if existing and not reset:
            click.echo(
                f"Seed already present (batch ids: {', '.join(existing)}). "
                f"Re-run with --reset to replace, or --status to inspect.",
                err=True,
            )
            sys.exit(0)

        if reset and existing:
            deleted = _delete_seed_rows(s)
            click.echo(f"  Removed {deleted} seeded batch(es).")

        try:
            batch, claims, activity, remittances = _build_seed_rows(count)
        except Exception as exc:
            click.echo(f"Failed to build seed rows: {exc}", err=True)
            sys.exit(1)
        try:
            _insert(batch, claims, activity, remittances)
        except Exception as exc:
            click.echo(f"Failed to insert seed rows: {exc}", err=True)
            sys.exit(1)

        click.echo(f"  Inserted batch {batch.id} with {len(claims)} claims, "
                   f"{len(remittances)} remittances, {len(activity)} activity events.")
        _print_status(s)