"""Import manually-logged activities (strength, hikes, unrecorded runs) from CSV.

Garmin-sourced activities live in `activities`; this module writes to a
parallel `manual_activities` table so re-ingesting from Garmin can never
clobber hand-entered rows. The two tables are unioned at read time by
`daily_training_load_series(..., include_manual=True)`.

CSV schema (header row required):

    activity_date,activity_type,distance_km,duration_min,training_load,notes,external_id

Only `activity_date` and `activity_type` are required; other columns are
optional and may be blank. `external_id`, when supplied, makes re-imports
idempotent (upsert on conflict).

Usage:
    uv run openrun-import-manual workouts.csv
    uv run openrun-import-manual workouts.csv --dry-run
"""

from __future__ import annotations

import argparse
import csv
import sqlite3
import sys
from dataclasses import dataclass
from datetime import date as Date, datetime
from pathlib import Path

from ..db import connect


REQUIRED_FIELDS = ("activity_date", "activity_type")
OPTIONAL_FIELDS = ("distance_km", "duration_min", "training_load", "notes", "external_id")


@dataclass(frozen=True)
class ImportResult:
    inserted: int
    updated: int
    skipped: int
    errors: list[str]


def _parse_float(s: str | None) -> float | None:
    if s is None or s == "":
        return None
    return float(s)


def _parse_date(s: str) -> str:
    """Normalise to ISO YYYY-MM-DD. Accepts ISO date or ISO datetime."""
    s = s.strip()
    # Allow a YYYY-MM-DD or full ISO datetime; take only the date part.
    parsed = datetime.fromisoformat(s) if "T" in s or " " in s else Date.fromisoformat(s)
    if isinstance(parsed, datetime):
        parsed = parsed.date()
    return parsed.isoformat()


def import_csv(conn: sqlite3.Connection, csv_path: Path, *, source: str | None = None) -> ImportResult:
    """Read a CSV file and upsert each row into `manual_activities`.

    Rows with a non-empty `external_id` are upserted on that key; rows without
    one are always inserted (so the user can deliberately log a duplicate by
    leaving external_id blank).
    """
    inserted = updated = skipped = 0
    errors: list[str] = []
    src_label = source or str(csv_path)

    with csv_path.open(newline="") as fh:
        reader = csv.DictReader(fh)
        missing = [f for f in REQUIRED_FIELDS if f not in (reader.fieldnames or [])]
        if missing:
            raise ValueError(
                f"CSV is missing required columns: {missing}. "
                f"Got: {reader.fieldnames}"
            )

        for line_no, row in enumerate(reader, start=2):
            try:
                date_iso = _parse_date(row["activity_date"])
                atype = row["activity_type"].strip()
                if not atype:
                    raise ValueError("activity_type is empty")
                distance = _parse_float(row.get("distance_km"))
                duration = _parse_float(row.get("duration_min"))
                tl = _parse_float(row.get("training_load"))
                notes = (row.get("notes") or "").strip() or None
                ext = (row.get("external_id") or "").strip() or None
            except (KeyError, ValueError) as exc:
                errors.append(f"line {line_no}: {exc}")
                skipped += 1
                continue

            if ext is not None:
                existing = conn.execute(
                    "SELECT id FROM manual_activities WHERE external_id = ?", (ext,)
                ).fetchone()
                if existing is not None:
                    conn.execute(
                        """UPDATE manual_activities SET
                             activity_date=?, activity_type=?, distance_km=?,
                             duration_min=?, training_load=?, notes=?,
                             source=?, imported_at=datetime('now')
                           WHERE external_id=?""",
                        (date_iso, atype, distance, duration, tl, notes, src_label, ext),
                    )
                    updated += 1
                    continue

            conn.execute(
                """INSERT INTO manual_activities
                     (activity_date, activity_type, distance_km, duration_min,
                      training_load, notes, external_id, source, imported_at)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))""",
                (date_iso, atype, distance, duration, tl, notes, ext, src_label),
            )
            inserted += 1

    conn.commit()
    return ImportResult(inserted=inserted, updated=updated, skipped=skipped, errors=errors)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("csv_path", help="Path to a CSV file of manual activities")
    parser.add_argument("--dry-run", action="store_true",
                        help="Parse the CSV but don't write to the DB")
    args = parser.parse_args()

    csv_path = Path(args.csv_path).expanduser().resolve()
    if not csv_path.is_file():
        sys.exit(f"file not found: {csv_path}")

    if args.dry_run:
        # Use an in-memory throwaway connection so we still exercise the schema.
        conn = sqlite3.connect(":memory:")
        from .. import db as _db
        conn.executescript(_db.SCHEMA)
    else:
        conn = connect()
    try:
        result = import_csv(conn, csv_path)
    finally:
        conn.close()

    print(f"manual import — {csv_path.name}")
    print(f"  inserted : {result.inserted}")
    print(f"  updated  : {result.updated}")
    print(f"  skipped  : {result.skipped}")
    if result.errors:
        print("  errors   :")
        for e in result.errors[:20]:
            print(f"    {e}")
        if len(result.errors) > 20:
            print(f"    … and {len(result.errors) - 20} more")


if __name__ == "__main__":
    main()
