"""CSV import helpers for the Autotouch CLI.

Functions for CSV parsing, import assertions (row-count, required-columns,
duplicate-key, non-empty checks), and the multi-step row-create + cell-patch
workflow used by ``rows import-csv``.
"""

from __future__ import annotations

import argparse
import csv
from typing import Any, Callable, Dict, List, Optional, Tuple

from autotouch_cli.core.http import ApiRequestError
from autotouch_cli.exceptions import AutotouchAPIError, AutotouchInputError

__all__ = [
    "read_records_from_csv",
    "parse_csv_flag_values",
    "parse_non_empty_requirements",
    "is_non_empty_cell_value",
    "collect_import_assertions",
    "has_import_assertions",
    "evaluate_import_assertions_on_records",
    "build_import_verify_query_params",
    "create_rows_and_patch_records",
]


# ---------------------------------------------------------------------------
# CSV parsing
# ---------------------------------------------------------------------------


def read_records_from_csv(
    file_path: str,
    *,
    delimiter: str = ",",
    encoding: str = "utf-8",
    trim_headers: bool = True,
    trim_values: bool = False,
    empty_as_null: bool = True,
    skip_empty_rows: bool = True,
    limit: Optional[int] = None,
) -> List[Dict[str, Any]]:
    records: List[Dict[str, Any]] = []
    try:
        with open(file_path, "r", encoding=encoding, newline="") as f:
            reader = csv.DictReader(f, delimiter=delimiter)
            if not reader.fieldnames:
                raise AutotouchInputError("CSV file has no header row")

            normalized_headers = []
            for header in reader.fieldnames:
                key = (header or "")
                key = key.strip() if trim_headers else key
                normalized_headers.append(key)
            reader.fieldnames = normalized_headers

            for raw_row in reader:
                row_obj: Dict[str, Any] = {}
                for key, value in (raw_row or {}).items():
                    col_key = str(key or "").strip() if trim_headers else str(key or "")
                    if not col_key:
                        continue
                    cell_value: Any = value
                    if isinstance(cell_value, str) and trim_values:
                        cell_value = cell_value.strip()
                    if empty_as_null and (cell_value is None or (isinstance(cell_value, str) and cell_value.strip() == "")):
                        cell_value = None
                    row_obj[col_key] = cell_value

                if skip_empty_rows:
                    has_value = any(
                        value is not None and (not isinstance(value, str) or value.strip() != "")
                        for value in row_obj.values()
                    )
                    if not has_value:
                        continue

                records.append(row_obj)
                if limit is not None and limit > 0 and len(records) >= limit:
                    break
    except FileNotFoundError:
        raise AutotouchInputError(f"CSV file not found: {file_path}")
    except AutotouchInputError:
        raise
    except Exception as exc:
        raise AutotouchInputError(f"failed to read CSV '{file_path}': {exc}") from exc
    return records


# ---------------------------------------------------------------------------
# Flag / argument parsing helpers
# ---------------------------------------------------------------------------


def parse_csv_flag_values(values: Optional[List[str]]) -> List[str]:
    items: List[str] = []
    seen = set()
    for raw in values or []:
        for part in str(raw or "").split(","):
            token = str(part or "").strip()
            if not token or token in seen:
                continue
            seen.add(token)
            items.append(token)
    return items


def parse_non_empty_requirements(values: Optional[List[str]]) -> Dict[str, float]:
    requirements: Dict[str, float] = {}
    for raw in values or []:
        for part in str(raw or "").split(","):
            token = str(part or "").strip()
            if not token:
                continue
            if ":" not in token:
                raise AutotouchInputError(
                    f"invalid --require-non-empty token '{token}'. Use key:ratio (for example post_content:0.95)."
                )
            key, ratio_raw = token.split(":", 1)
            column_key = str(key or "").strip()
            if not column_key:
                raise AutotouchInputError(f"invalid --require-non-empty token '{token}' (missing key)")
            try:
                ratio = float(ratio_raw)
            except ValueError:
                raise AutotouchInputError(f"invalid --require-non-empty ratio in token '{token}'")
            if ratio < 0 or ratio > 1:
                raise AutotouchInputError(f"invalid --require-non-empty ratio in token '{token}' (expected 0..1)")
            requirements[column_key] = ratio
    return requirements


def is_non_empty_cell_value(value: Any) -> bool:
    if value is None:
        return False
    if isinstance(value, str):
        return bool(value.strip())
    if isinstance(value, (list, tuple, set, dict)):
        return len(value) > 0
    return True


# ---------------------------------------------------------------------------
# Import assertions
# ---------------------------------------------------------------------------


def collect_import_assertions(args: argparse.Namespace) -> Dict[str, Any]:
    expected_rows_raw = getattr(args, "expected_rows", None)
    expected_rows: Optional[int] = None
    if expected_rows_raw is not None:
        try:
            expected_rows = int(expected_rows_raw)
        except (TypeError, ValueError):
            raise AutotouchInputError("--expected-rows must be an integer")
        if expected_rows < 0:
            raise AutotouchInputError("--expected-rows must be >= 0")

    return {
        "expected_rows": expected_rows,
        "required_columns": parse_csv_flag_values(getattr(args, "require_columns", None)),
        "duplicate_key": parse_csv_flag_values(getattr(args, "duplicate_key", None)),
        "require_non_empty": parse_non_empty_requirements(getattr(args, "require_non_empty", None)),
    }


def has_import_assertions(assertions: Dict[str, Any]) -> bool:
    return bool(
        assertions.get("expected_rows") is not None
        or assertions.get("required_columns")
        or assertions.get("duplicate_key")
        or assertions.get("require_non_empty")
    )


def evaluate_import_assertions_on_records(
    *,
    records: List[Dict[str, Any]],
    assertions: Dict[str, Any],
    source: str,
) -> Dict[str, Any]:
    expected_rows = assertions.get("expected_rows")
    required_columns = list(assertions.get("required_columns") or [])
    duplicate_key = list(assertions.get("duplicate_key") or [])
    require_non_empty = dict(assertions.get("require_non_empty") or {})

    headers: List[str] = []
    header_seen = set()
    for record in records:
        if not isinstance(record, dict):
            continue
        for key in record.keys():
            key_str = str(key or "").strip()
            if not key_str or key_str in header_seen:
                continue
            header_seen.add(key_str)
            headers.append(key_str)

    checks: Dict[str, Any] = {}
    failures: List[Dict[str, Any]] = []
    row_count = len(records)

    if expected_rows is not None:
        passed = int(row_count) == int(expected_rows)
        checks["expected_rows"] = {
            "expected": int(expected_rows),
            "actual": int(row_count),
            "passed": passed,
        }
        if not passed:
            failures.append(
                {
                    "check": "expected_rows",
                    "message": f"Expected {expected_rows} rows but parsed {row_count}.",
                }
            )

    if required_columns:
        missing = [key for key in required_columns if key not in header_seen]
        passed = len(missing) == 0
        checks["required_columns"] = {
            "required": required_columns,
            "missing": missing,
            "passed": passed,
        }
        if not passed:
            failures.append(
                {
                    "check": "required_columns",
                    "message": f"Missing required columns: {', '.join(missing)}",
                }
            )

    if duplicate_key:
        missing_duplicate_columns = [key for key in duplicate_key if key not in header_seen]
        duplicate_count = 0
        sample_groups: List[Dict[str, Any]] = []
        passed = True
        if missing_duplicate_columns:
            passed = False
            failures.append(
                {
                    "check": "duplicate_key",
                    "message": f"Duplicate-key columns missing in CSV: {', '.join(missing_duplicate_columns)}",
                }
            )
        else:
            grouped: Dict[Tuple[str, ...], List[int]] = {}
            for idx, record in enumerate(records):
                if not isinstance(record, dict):
                    continue
                parts: List[str] = []
                has_value = False
                for key in duplicate_key:
                    value = record.get(key)
                    normalized = str(value or "").strip().lower()
                    if normalized:
                        has_value = True
                    parts.append(normalized)
                if not has_value:
                    continue
                signature = tuple(parts)
                grouped.setdefault(signature, []).append(idx + 1)

            duplicates = [indices for indices in grouped.values() if len(indices) > 1]
            if duplicates:
                passed = False
                duplicate_count = sum(max(0, len(indices) - 1) for indices in duplicates)
                sample_groups = [{"size": len(indices), "row_numbers": indices[:5]} for indices in duplicates[:5]]
                failures.append(
                    {
                        "check": "duplicate_key",
                        "message": f"Found {duplicate_count} duplicate rows using key {duplicate_key}.",
                    }
                )
        checks["duplicate_key"] = {
            "keys": duplicate_key,
            "duplicates": int(duplicate_count),
            "sample_groups": sample_groups,
            "passed": passed,
        }

    if require_non_empty:
        non_empty_checks: Dict[str, Any] = {}
        for key, threshold in require_non_empty.items():
            if key not in header_seen:
                non_empty_checks[key] = {
                    "threshold": threshold,
                    "ratio": 0.0,
                    "non_empty_rows": 0,
                    "total_rows": row_count,
                    "passed": False,
                    "reason": "column_missing",
                }
                failures.append(
                    {
                        "check": "require_non_empty",
                        "message": f"Column '{key}' missing for non-empty requirement.",
                    }
                )
                continue

            non_empty_rows = 0
            for record in records:
                value = record.get(key) if isinstance(record, dict) else None
                if is_non_empty_cell_value(value):
                    non_empty_rows += 1
            ratio = (non_empty_rows / row_count) if row_count else 0.0
            passed = ratio >= threshold
            non_empty_checks[key] = {
                "threshold": threshold,
                "ratio": ratio,
                "non_empty_rows": non_empty_rows,
                "total_rows": row_count,
                "passed": passed,
            }
            if not passed:
                failures.append(
                    {
                        "check": "require_non_empty",
                        "message": (
                            f"Column '{key}' non-empty ratio {ratio:.4f} "
                            f"is below threshold {threshold:.4f}."
                        ),
                    }
                )
        checks["require_non_empty"] = non_empty_checks

    return {
        "source": source,
        "rows": row_count,
        "headers": headers,
        "checks": checks,
        "passed": len(failures) == 0,
        "failures": failures,
    }


def build_import_verify_query_params(assertions: Dict[str, Any]) -> Dict[str, Any]:
    params: Dict[str, Any] = {}
    expected_rows = assertions.get("expected_rows")
    if expected_rows is not None:
        params["expected_rows"] = int(expected_rows)
    required_columns = list(assertions.get("required_columns") or [])
    if required_columns:
        params["required_columns"] = ",".join(required_columns)
    duplicate_key = list(assertions.get("duplicate_key") or [])
    if duplicate_key:
        params["duplicate_key"] = ",".join(duplicate_key)
    require_non_empty = dict(assertions.get("require_non_empty") or {})
    if require_non_empty:
        parts = [f"{key}:{ratio:g}" for key, ratio in require_non_empty.items()]
        params["require_non_empty"] = ",".join(parts)
    return params


# ---------------------------------------------------------------------------
# Row creation + cell patching
# ---------------------------------------------------------------------------


def create_rows_and_patch_records(
    *,
    table_id: str,
    records: Optional[List[Dict[str, Any]]],
    blank_count: int,
    base_url: str,
    token: str,
    use_x_api_key: bool,
    timeout: int,
    verbose: bool,
    detect_types: bool,
    request_api: Callable[..., Any],
) -> Dict[str, Any]:
    created_row_ids: List[str] = []
    patch_updates: List[Dict[str, Any]] = []

    if records is not None:
        for idx, record in enumerate(records):
            if not isinstance(record, dict):
                raise AutotouchInputError(f"record at index {idx} is not a JSON object")
            row_result = request_api(
                "POST",
                f"/api/tables/{table_id}/rows",
                base_url=base_url,
                token=token,
                use_x_api_key=use_x_api_key,
                timeout=timeout,
                verbose=verbose,
            )
            row_id = (row_result or {}).get("rowId") if isinstance(row_result, dict) else None
            if not row_id:
                raise AutotouchAPIError(f"row create response missing rowId: {row_result}")
            row_id = str(row_id)
            created_row_ids.append(row_id)
            for key, value in record.items():
                patch_updates.append({"rowId": row_id, "key": str(key), "value": value})
    else:
        count = max(0, int(blank_count or 0))
        if count <= 0:
            raise AutotouchInputError("--count must be > 0")
        for _ in range(count):
            row_result = request_api(
                "POST",
                f"/api/tables/{table_id}/rows",
                base_url=base_url,
                token=token,
                use_x_api_key=use_x_api_key,
                timeout=timeout,
                verbose=verbose,
            )
            row_id = (row_result or {}).get("rowId") if isinstance(row_result, dict) else None
            if not row_id:
                raise AutotouchAPIError(f"row create response missing rowId: {row_result}")
            created_row_ids.append(str(row_id))

    patch_result: Optional[Any] = None
    if patch_updates:
        params = {"detect_types": "true"} if detect_types else None
        try:
            patch_result = request_api(
                "PATCH",
                f"/api/tables/{table_id}/cells",
                base_url=base_url,
                token=token,
                use_x_api_key=use_x_api_key,
                payload={"updates": patch_updates},
                params=params,
                timeout=timeout,
                verbose=verbose,
                raise_on_error=True,
            )
        except ApiRequestError as exc:
            return {
                "created": len(created_row_ids),
                "rowIds": created_row_ids,
                "updatedCells": 0,
                "attemptedCellUpdates": len(patch_updates),
                "patchResult": None,
                "patchFailed": True,
                "patchError": exc.as_dict(),
                "hint": (
                    "Rows were created before cell patching failed. "
                    "Fix the payload and retry, or delete the created rowIds."
                ),
            }

    return {
        "created": len(created_row_ids),
        "rowIds": created_row_ids,
        "updatedCells": len(patch_updates),
        "patchResult": patch_result,
    }
