"""Shared canonicalization for the migration diff harness.

Both sides of the comparison (v1 SQLite snapshot JSON and the v2 Postgres
export) are normalized through THESE functions — never two implementations,
so a parsing quirk can't produce a false match or false diff.

Canonical forms:
- timestamps -> UTC ISO-8601 with seconds precision ("2026-05-27T22:52:20+00:00")
- date-only values -> "YYYY-MM-DD"
- booleans -> True/False (v1 0/1 integers normalized)
- None stays None; empty string stays empty string (they are DIFFERENT —
  v1 distinguishes them and so must v2)
"""
from __future__ import annotations

from datetime import date, datetime, timezone

# Per-table column type hints for canonicalization.
# ts = timestamp-ish, d = date-only, b = boolean-ish.
V1_COLUMN_KINDS: dict[str, dict[str, str]] = {
    "activity_log": {"created_at": "ts"},
    "area_stations": {},
    "areas": {"created_at": "ts", "is_archived": "b"},
    "artifacts": {"generated_at": "d"},
    "chat_messages": {"created_at": "ts"},
    "chat_sessions": {"created_at": "ts", "ended_at": "ts"},
    "field_note_targets": {"is_primary": "b"},
    "field_notes": {
        "created_at": "ts", "captured_at": "ts", "processed_at": "ts",
        "comment_date": "d", "is_archived": "b",
    },
    "overview_versions": {"created_at": "ts"},
    "plantings": {"created_at": "ts", "is_archived": "b"},
    "plants": {"created_at": "ts", "is_archived": "b"},
    "species": {"created_at": "ts", "is_archived": "b"},
    "supplies": {"created_at": "ts", "is_archived": "b", "needed": "b"},
    "tool_suggestions": {"created_at": "ts"},
    "varieties": {"created_at": "ts", "is_archived": "b"},
    "watering_stations": {"created_at": "ts", "is_archived": "b"},
    "years": {"created_at": "ts", "is_archived": "b"},
}

# v2-only columns EXCLUDED from comparison, per table. Anything else
# unexpected on either side is a FAILURE (explicit allowlist, no skips).
V2_ONLY_COLUMNS: dict[str, set] = {
    "areas": {"garden_id"},
    "watering_stations": {"garden_id"},
    "supplies": {"garden_id"},
    "years": {"garden_id"},
    "species": {
        "garden_id", "mature_spacing_sqft", "rotation_family", "sun_pref",
        "life_cycle", "edible_part", "notes_local",
    },
    "varieties": {"mature_spacing_sqft", "harvest_window", "flavor_notes"},
    "plantings": {"garden_id"},
    "plants": {
        "garden_id", "flavor_rating", "vigor_rating", "would_plant_again",
        "outcome_summary",
    },
    "field_notes": {"garden_id", "page_context"},
    "chat_sessions": {"garden_id", "status", "resolution", "resolution_note_id"},
    "artifacts": {"garden_id"},
    "activity_log": {"garden_id"},
}


def parse_ts(value) -> datetime | None:
    """Parse v1's timestamp strings (or PG datetimes) to aware-UTC."""
    if value is None or value == "":
        return None
    if isinstance(value, datetime):
        dt = value
    else:
        s = str(value).strip().replace("Z", "+00:00")
        dt = datetime.fromisoformat(s)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)  # v1 naive strings are UTC
    return dt.astimezone(timezone.utc)


def canon_ts(value) -> str | None:
    dt = parse_ts(value)
    return None if dt is None else dt.isoformat(timespec="seconds")


def parse_date(value) -> date | None:
    if value is None or value == "":
        return None
    if isinstance(value, date) and not isinstance(value, datetime):
        return value
    if isinstance(value, datetime):
        return value.date()
    return date.fromisoformat(str(value).strip()[:10])


def canon_date(value) -> str | None:
    d = parse_date(value)
    return None if d is None else d.isoformat()


def canon_bool(value) -> bool | None:
    if value is None:
        return None
    if isinstance(value, bool):
        return value
    return bool(int(value))


def canon_row(table: str, row: dict, *, drop_v2_only: bool = False) -> dict:
    """Normalize one row dict to canonical comparison form."""
    kinds = V1_COLUMN_KINDS.get(table, {})
    v2only = V2_ONLY_COLUMNS.get(table, set()) if drop_v2_only else set()
    out = {}
    for key, value in row.items():
        if key in v2only:
            continue
        kind = kinds.get(key)
        if kind == "ts":
            out[key] = canon_ts(value)
        elif kind == "d":
            out[key] = canon_date(value)
        elif kind == "b":
            out[key] = canon_bool(value)
        elif isinstance(value, float):
            # SQLite REAL vs PG double: normalize repr via repr-round-trip
            out[key] = float(f"{value!r}")
        else:
            out[key] = value
    return out
