"""cortex/cli/io_cmds.py — export, export-memory, import-memory, batch-ingest."""
from __future__ import annotations

import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional

import click

import cortex.cli.app as _app

main = _app.main


# ---------------------------------------------------------------------------
# evaos export
# ---------------------------------------------------------------------------

@main.command("export")
@click.option("--format", "fmt", default="json",
              type=click.Choice(["json", "sql", "sqlite"]), show_default=True,
              help="Export format.")
@click.option("--output", "-o", default="-", show_default=True,
              help="Output file path (default: stdout).")
@click.pass_context
def cmd_export(ctx: click.Context, fmt: str, output: str) -> None:
    """Export all memories as JSON, SQL INSERT statements, or SQLite database copy.

    Example:

    \b
        evaos export --format json --output memories.json
        evaos export --format sql | gzip > backup.sql.gz
        evaos export --format sqlite --output backup.db
    """
    plugin = _app._make_plugin(ctx.obj["db"], ctx.obj["config"])

    async def _export() -> None:
        if plugin.storage:
            await plugin.storage.initialize()
        storage = getattr(plugin, "storage", None)
        if storage is None:
            _app._err("Storage not available")
            sys.exit(1)

        # SQLite binary export — uses backup API, no claim iteration needed
        if fmt == "sqlite":
            if output == "-":
                _app._err("SQLite export requires --output path (cannot write binary to stdout)")
                sys.exit(1)
            from cortex.tools.export_import import export_sqlite
            try:
                result_path = await export_sqlite(storage, output_path=output)
                size_kb = Path(result_path).stat().st_size // 1024
                _app._ok(f"SQLite database exported to {result_path} ({size_kb} KB)")
            except Exception as exc:
                _app._err(f"SQLite export failed: {exc}")
                sys.exit(1)
            return

        claims: List[dict] = []

        try:
            async with storage._get_connection() as conn:  # type: ignore[attr-defined]
                async with conn.execute("SELECT * FROM semantic_claims") as cur:
                    column_names = [d[0] for d in cur.description]
                    async for row in cur:
                        claims.append(dict(zip(column_names, row)))
        except Exception as exc:
            _app._err(f"Failed to read claims: {exc}")
            sys.exit(1)

        if fmt == "json":
            text = json.dumps(claims, indent=2, default=str)
        else:  # sql
            lines = [f"-- Cortex export — {datetime.now(timezone.utc).isoformat()}",
                     "-- Format: SQL INSERT"]
            if claims:
                cols = ", ".join(f'"{c}"' for c in claims[0].keys())
                for claim in claims:
                    vals = ", ".join(
                        "NULL" if v is None else f"'{str(v).replace(chr(39), chr(39)+chr(39))}'"
                        for v in claim.values()
                    )
                    lines.append(f"INSERT INTO semantic_claims ({cols}) VALUES ({vals});")
            text = "\n".join(lines) + "\n"

        if output == "-":
            click.echo(text)
        else:
            Path(output).write_text(text)
            _app._ok(f"Exported {len(claims)} claim(s) to {output}")

    _app._run(_export())


# ---------------------------------------------------------------------------
# evaos export-memory (enhanced — json|csv|markdown)
# ---------------------------------------------------------------------------

@main.command("export-memory")
@click.option("--format", "fmt", default="json",
              type=click.Choice(["json", "csv", "markdown"]), show_default=True,
              help="Export format.")
@click.option("--output", "-o", required=True,
              help="Output file path.")
@click.option("--owner-id", default="default", show_default=True,
              help="Owner ID to export memories for.")
@click.pass_context
def cmd_export_memory(ctx: click.Context, fmt: str, output: str, owner_id: str) -> None:
    """Export memories as JSON, CSV, or Markdown.

    \b
        evaos export-memory --format json --output memories.json
        evaos export-memory --format csv --output claims.csv
        evaos export-memory --format markdown --output memories.md --owner-id eva
    """
    from cortex.tools.export_import import export_json, export_csv, export_markdown

    async def _run_export() -> None:
        plugin = await _app._init_plugin(ctx.obj["db"], ctx.obj["config"])
        storage = getattr(plugin, "storage", None)
        if storage is None:
            _app._err("Storage not available")
            sys.exit(1)

        try:
            if fmt == "json":
                count = await export_json(storage, owner_id=owner_id, output_path=output)
            elif fmt == "csv":
                count = await export_csv(storage, owner_id=owner_id, output_path=output)
            else:
                count = await export_markdown(storage, owner_id=owner_id, output_path=output)
            _app._ok(f"Exported {count} claim(s) [{fmt}] → {output}")
        except Exception as exc:
            _app._err(f"Export failed: {exc}")
            sys.exit(1)

    _app._run(_run_export())


# ---------------------------------------------------------------------------
# evaos import-memory
# ---------------------------------------------------------------------------

@main.command("import-memory")
@click.option("--format", "fmt", default="evaos",
              type=click.Choice(["evaos", "mem0"]), show_default=True,
              help="Import format: evaos (JSON) or mem0 (JSONL).")
@click.option("--input", "-i", "input_path", required=True,
              help="Input file path.")
@click.option("--owner-id", default="default", show_default=True,
              help="Target owner ID for imported memories.")
@click.option("--dry-run", is_flag=True, default=False,
              help="Validate and count without writing to storage.")
@click.option("--on-conflict", "on_conflict", default="skip",
              type=click.Choice(["skip", "overwrite", "merge"]), show_default=True,
              help="Conflict resolution: skip (default), overwrite, or merge.")
@click.pass_context
def cmd_import_memory(
    ctx: click.Context, fmt: str, input_path: str, owner_id: str,
    dry_run: bool, on_conflict: str,
) -> None:
    """Import memories from evaOS JSON or Mem0 JSONL export.

    \b
        evaos import-memory --format evaos --input memories.json
        evaos import-memory --format mem0 --input mem0_export.jsonl --dry-run
        evaos import-memory --format evaos --input export.json --on-conflict overwrite
    """
    from cortex.tools.export_import import import_json, import_mem0

    async def _run_import() -> None:
        plugin = await _app._init_plugin(ctx.obj["db"], ctx.obj["config"])
        storage = getattr(plugin, "storage", None)
        if storage is None:
            _app._err("Storage not available")
            sys.exit(1)

        if dry_run:
            _app._warn("DRY RUN — no data will be written")

        try:
            if fmt == "evaos":
                result = await import_json(
                    storage, input_path=input_path, owner_id=owner_id,
                    dry_run=dry_run, on_conflict=on_conflict,
                )
            else:
                result = await import_mem0(
                    storage, input_path=input_path, owner_id=owner_id, dry_run=dry_run
                )
        except Exception as exc:
            _app._err(f"Import failed: {exc}")
            sys.exit(1)

        _app._ok(f"Imported: {result.imported}  Skipped: {result.skipped}  Errors: {len(result.errors)}")
        if result.errors:
            _app._warn(f"Errors ({len(result.errors)}):")
            for err in result.errors[:10]:
                click.echo(f"  • {err}")
            if len(result.errors) > 10:
                click.echo(f"  … and {len(result.errors) - 10} more")

    _app._run(_run_import())


# ---------------------------------------------------------------------------
# evaos backup — REMOVED (#1556)
# ---------------------------------------------------------------------------
# The simple shutil.copy2 backup command that was here has been removed.
# Use the hardened backup command in cortex/cli/backup_cmds.py instead,
# which provides WAL checkpointing, SHA-256 checksums, tar.gz archives,
# and integrity verification.
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# evaos batch-ingest
# ---------------------------------------------------------------------------

@main.command("batch-ingest")
@click.option(
    "--source",
    required=True,
    type=click.Choice(["sessions", "file", "text", "lcm"], case_sensitive=False),
    help="Source type: 'sessions' (OpenClaw JSONL logs), 'file' (markdown/text), 'text' (raw string), 'lcm' (LCM SQLite DB).",
)
@click.option(
    "--sessions-dir",
    default="~/.openclaw/agents/main/sessions",
    show_default=True,
    help="Path to OpenClaw sessions directory (source=sessions only).",
)
@click.option(
    "--from", "from_time",
    default=None,
    help="Start of time range, ISO 8601 (e.g. 2026-03-13T02:00:00Z). Source=sessions only.",
)
@click.option(
    "--to", "to_time",
    default=None,
    help="End of time range, ISO 8601 or 'now'. Source=sessions only.",
)
@click.option(
    "--path",
    "file_path",
    default=None,
    help="Path to file (source=file only).",
)
@click.option(
    "--text",
    default=None,
    help="Raw text to ingest (source=text only).",
)
@click.option(
    "--owner",
    required=True,
    default=None,
    envvar="CORTEX_OWNER_ID",
    help="Memory owner namespace (e.g. eva-origin). Required. Falls back to CORTEX_OWNER_ID env.",
)
@click.option(
    "--url",
    default=None,
    envvar="CORTEX_URL",
    help="Cortex HTTP base URL (e.g. https://cortex-electricsheep.fly.dev). Falls back to CORTEX_URL env. "
         "If omitted, uses local DB.",
)
@click.option(
    "--api-key",
    default=None,
    envvar="CORTEX_API_KEY",
    help="API key for Cortex HTTP auth. Falls back to CORTEX_API_KEY env.",
)
@click.option(
    "--dry-run",
    is_flag=True,
    default=False,
    help="Print what would be sent without actually POSTing.",
)
@click.option(
    "--reset-checkpoint",
    is_flag=True,
    default=False,
    help="Clear the idempotency checkpoint before running (re-process everything).",
)
@click.option(
    "--checkpoint",
    default="/tmp/batch-ingest-checkpoint.json",
    show_default=True,
    help="Path to checkpoint file for idempotency tracking.",
)
@click.option(
    "--quiet",
    is_flag=True,
    default=False,
    help="Suppress per-chunk progress output.",
)
@click.option(
    "--exclude-categories",
    default=None,
    help="Comma-separated list of claim categories to drop (e.g. 'engineering,misc').",
)
@click.option(
    "--lcm-db",
    default=None,
    help="Path to LCM SQLite database (source=lcm only). Default: ~/.openclaw/lcm.db",
)
@click.option(
    "--min-score",
    "lcm_min_score",
    default=15,
    show_default=True,
    type=int,
    help="Minimum emotional score threshold for LCM messages (source=lcm only).",
)
@click.option(
    "--lcm-limit",
    default=None,
    type=int,
    help="Cap number of LCM messages to scan (source=lcm only). Useful for testing.",
)
@click.pass_context
def cmd_batch_ingest(
    ctx: click.Context,
    source: str,
    sessions_dir: str,
    from_time: Optional[str],
    to_time: Optional[str],
    file_path: Optional[str],
    text: Optional[str],
    owner: Optional[str],
    url: Optional[str],
    api_key: Optional[str],
    dry_run: bool,
    reset_checkpoint: bool,
    checkpoint: str,
    quiet: bool,
    exclude_categories: Optional[str],
    lcm_db: Optional[str],
    lcm_min_score: int,
    lcm_limit: Optional[int],
) -> None:
    """Batch-ingest memories from session logs, files, or raw text.

    Reads from SOURCE, chunks the content, and sends each chunk through the
    Cortex extraction pipeline. Tracks progress with idempotency so it's safe
    to re-run.

    Examples:

    \b
        # Recover today's sessions since 2am UTC
        evaos batch-ingest --source sessions \\
            --from "2026-03-13T02:00:00Z" --to now \\
            --owner eva-origin \\
            --url https://cortex-electricsheep.fly.dev

    \b
        # Ingest a memory log file
        evaos batch-ingest --source file \\
            --path memory/2026-03-13.md \\
            --owner eva-origin

    \b
        # Ingest raw text
        evaos batch-ingest --source text \\
            --text "Andrew prefers dark mode" \\
            --owner eva-origin

    \b
        # Ingest emotionally-scored messages from LCM (dry run)
        evaos batch-ingest --source lcm --min-score 15 --dry-run

    \b
        # LCM to production Cortex (use --min-score >= 15 to avoid
        # false positives from internal system/config messages)
        evaos batch-ingest --source lcm --min-score 15 \\
            --owner eva-origin \\
            --url https://cortex-electricsheep.fly.dev
    """
    try:
        from cortex.batch_ingest import run_batch_ingest
    except ImportError as exc:
        _app._err(f"Cannot import batch_ingest: {exc}")
        sys.exit(1)

    # Prefer: explicit --owner → CORTEX_OWNER_ID env (already resolved by Click)
    # owner is required=True so it will never be None here unless env var is also unset.
    resolved_owner = owner
    if not resolved_owner:
        _app._err(
            "Error: --owner / CORTEX_OWNER_ID is required. "
            "Specify the memory owner namespace explicitly (e.g. --owner eva-origin)."
        )
        sys.exit(1)

    try:
        # Parse exclude-categories into a set
        _excl_cats = (
            {c.strip().lower() for c in exclude_categories.split(",") if c.strip()}
            if exclude_categories else None
        )

        summary = run_batch_ingest(
            source=source,
            sessions_dir=sessions_dir,
            from_time=from_time,
            to_time=to_time,
            file_path=file_path,
            text=text,
            owner=resolved_owner,
            url=url,
            api_key=api_key,
            db=ctx.obj.get("db"),
            config=ctx.obj.get("config"),
            dry_run=dry_run,
            verbose=not quiet,
            reset_checkpoint=reset_checkpoint,
            checkpoint_path=checkpoint,
            exclude_categories=_excl_cats,
            lcm_db=lcm_db,
            lcm_min_score=lcm_min_score,
            lcm_limit=lcm_limit,
        )
    except (ValueError, FileNotFoundError, RuntimeError) as exc:
        _app._err(str(exc))
        sys.exit(1)

    if summary.failed > 0 and summary.succeeded == 0:
        sys.exit(1)
