"""Helpers to import CSV/Excel data into the project's SQLite DB.

This module provides a small, dependency-light importer that supports CSV out of the
box and Excel (.xlsx) when ``openpyxl`` is installed. It intentionally keeps the
mapping simple (header-driven) and provides safe deduplication for normalized
keyword/angel/demon tables.
"""
from __future__ import annotations

import csv
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple

try:
    from openpyxl import load_workbook  # type: ignore
except Exception:  # pragma: no cover - optional dependency
    load_workbook = None  # type: ignore

from tarot.card.details import CardDetailsRegistry


# Simple helpers -----------------------------------------------------------

def _read_csv(path: Path, delimiter: str = ",") -> Iterable[Dict[str, str]]:
    with path.open("r", encoding="utf-8-sig", newline="") as fh:
        reader = csv.DictReader(fh, delimiter=delimiter)
        for row in reader:
            yield {k.strip(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()}


def _read_excel(path: Path, sheet: Optional[str] = None) -> Iterable[Dict[str, str]]:
    if load_workbook is None:
        raise RuntimeError("openpyxl is required to import Excel files - pip install openpyxl")
    wb = load_workbook(path, read_only=True, data_only=True)
    ws = wb[sheet] if sheet and sheet in wb.sheetnames else wb[wb.sheetnames[0]]
    it = ws.iter_rows(values_only=True)
    try:
        headers = next(it)
    except StopIteration:
        return
    headers = [h.strip() if isinstance(h, str) else str(h) for h in headers]
    for row in it:
        yield {str(h).strip(): (str(v).strip() if v is not None else "") for h, v in zip(headers, row)}


def _get_conn(db_path: Optional[Path]) -> sqlite3.Connection:
    if db_path is None:
        reg = CardDetailsRegistry(use_db=True)
        db_path = reg._db_path  # use the same resolution logic
    else:
        # Ensure DB is seeded if missing
        CardDetailsRegistry(use_db=True, db_path=db_path)
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    return conn


def _coerce_bool(val: Optional[str]) -> int:
    if val is None:
        return 0
    if isinstance(val, bool):
        return 1 if val else 0
    s = str(val).strip().lower()
    return 1 if s in {"1", "true", "yes", "y", "t"} else 0


def _parse_date_to_parts(val: Optional[str]) -> Optional[Tuple[int, int]]:
    if not val:
        return None
    s = str(val).strip()
    if not s:
        return None
    # Accept formats: M/D, MM/DD, YYYY-MM-DD
    if "/" in s:
        parts = s.split("/")
        if len(parts) >= 2:
            try:
                m = int(parts[0])
                d = int(parts[1])
                return (m, d)
            except Exception:
                return None
    if "-" in s:
        parts = s.split("-")
        try:
            m = int(parts[1])
            d = int(parts[2])
            return (m, d)
        except Exception:
            return None
    return None


# Table-specific importers -----------------------------------------------

@dataclass
class ImportResult:
    inserted: int = 0
    skipped: int = 0
    updated: int = 0


def import_keywords(rows: Iterable[Dict[str, str]], db_path: Optional[Path] = None) -> ImportResult:
    conn = _get_conn(db_path)
    cur = conn.cursor()
    res = ImportResult()
    try:
        for row in rows:
            card_key = row.get("card_key") or row.get("registry_key") or row.get("card")
            keyword = row.get("keyword") or row.get("kw")
            is_rev = _coerce_bool(row.get("is_reversed") or row.get("reversed"))
            if not card_key or not keyword:
                res.skipped += 1
                continue
            # deduplicate
            exists = cur.execute(
                "SELECT 1 FROM card_keywords WHERE card_key=? AND keyword=? AND is_reversed=?",
                (card_key, keyword, is_rev),
            ).fetchone()
            if exists:
                res.skipped += 1
                continue
            cur.execute(
                "INSERT INTO card_keywords (card_key, keyword, is_reversed) VALUES (?, ?, ?)",
                (card_key, keyword, is_rev),
            )
            res.inserted += 1
        conn.commit()
    finally:
        conn.close()
    return res


def import_angels(rows: Iterable[Dict[str, str]], db_path: Optional[Path] = None) -> ImportResult:
    conn = _get_conn(db_path)
    cur = conn.cursor()
    res = ImportResult()
    try:
        for row in rows:
            card_key = row.get("card_key") or row.get("registry_key") or row.get("card")
            name = row.get("name") or row.get("angel")
            is_night = _coerce_bool(row.get("is_night") or row.get("night") or row.get("isNight"))
            if not card_key or not name:
                res.skipped += 1
                continue
            exists = cur.execute(
                "SELECT 1 FROM card_angels WHERE card_key=? AND name=? AND is_night=?",
                (card_key, name, is_night),
            ).fetchone()
            if exists:
                res.skipped += 1
                continue
            cur.execute(
                "INSERT INTO card_angels (card_key, name, is_night) VALUES (?, ?, ?)",
                (card_key, name, is_night),
            )
            res.inserted += 1
        conn.commit()
    finally:
        conn.close()
    return res


def import_demons(rows: Iterable[Dict[str, str]], db_path: Optional[Path] = None) -> ImportResult:
    conn = _get_conn(db_path)
    cur = conn.cursor()
    res = ImportResult()
    try:
        for row in rows:
            card_key = row.get("card_key") or row.get("registry_key") or row.get("card")
            name = row.get("name") or row.get("demon")
            is_night = _coerce_bool(row.get("is_night") or row.get("night") or row.get("isNight"))
            if not card_key or not name:
                res.skipped += 1
                continue
            exists = cur.execute(
                "SELECT 1 FROM card_demons WHERE card_key=? AND name=? AND is_night=?",
                (card_key, name, is_night),
            ).fetchone()
            if exists:
                res.skipped += 1
                continue
            cur.execute(
                "INSERT INTO card_demons (card_key, name, is_night) VALUES (?, ?, ?)",
                (card_key, name, is_night),
            )
            res.inserted += 1
        conn.commit()
    finally:
        conn.close()
    return res


def import_card_details(rows: Iterable[Dict[str, str]], db_path: Optional[Path] = None) -> ImportResult:
    conn = _get_conn(db_path)
    cur = conn.cursor()
    res = ImportResult()
    allowed = {
        "registry_key",
        "explanation_summary",
        "explanation_waite",
        "interpretation",
        "guidance",
        "date_start",
        "date_end",
        "date_start_month",
        "date_start_day",
        "date_end_month",
        "date_end_day",
        "degrees",
        "extra_json",
    }
    try:
        for row in rows:
            if not row:
                res.skipped += 1
                continue
            reg_key = row.get("registry_key") or row.get("card_key") or row.get("card")
            if not reg_key:
                res.skipped += 1
                continue
            # Process date short forms
            ds = _parse_date_to_parts(row.get("date_start"))
            de = _parse_date_to_parts(row.get("date_end"))
            if ds and de:
                row["date_start_month"], row["date_start_day"] = ds
                row["date_end_month"], row["date_end_day"] = de

            # Build columns/values to upsert
            cols = ["registry_key"]
            vals = [reg_key]
            up_cols = []
            up_vals = []
            for k, v in row.items():
                if k == "registry_key":
                    continue
                if k not in allowed:
                    continue
                if v is None or v == "":
                    continue
                cols.append(k)
                vals.append(v)
                up_cols.append(f"{k}=excluded.{k}")
            placeholders = ",".join("?" for _ in vals)
            sql = (
                f"INSERT INTO card_details ({','.join(cols)}) VALUES ({placeholders}) "
                f"ON CONFLICT(registry_key) DO UPDATE SET {', '.join(up_cols)}"
            )
            cur.execute(sql, tuple(vals))
            # rough heuristic: treat as inserted if no row existed
            if cur.rowcount > 0:
                res.inserted += 1
            else:
                res.updated += 1
        conn.commit()
    finally:
        conn.close()
    return res


# Public helper -----------------------------------------------------------

def import_file(
    path: Path,
    table: str,
    *,
    db_path: Optional[Path] = None,
    sheet: Optional[str] = None,
    delimiter: str = ",",
) -> ImportResult:
    """Import a CSV or Excel file into one of the supported tables.

    Supported tables: ``card_keywords``, ``card_angels``, ``card_demons``, ``card_details``.

    The importer is header-driven and will look for reasonable alternate column
    names (e.g., ``card_key`` / ``card`` / ``registry_key``).
    """
    if not path.exists():
        raise FileNotFoundError(path)
    suffix = path.suffix.lower()
    if suffix in {".xls", ".xlsx"}:
        rows = _read_excel(path, sheet)
    else:
        rows = _read_csv(path, delimiter)

    table = table.lower()
    if table == "card_keywords":
        return import_keywords(rows, db_path)
    if table == "card_angels":
        return import_angels(rows, db_path)
    if table == "card_demons":
        return import_demons(rows, db_path)
    if table == "card_details":
        return import_card_details(rows, db_path)
    raise ValueError(f"Unsupported table: {table}")
