"""Pipeline commands: run, seed, transform, stream, lint, watch, schedule."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Annotated, Optional

import typer
from rich.table import Table

from havn.cli import _load_config, _resolve_project, app, console

logger = logging.getLogger("havn.cli")


@app.command()
def run(
    script: Annotated[str, typer.Argument(help="Script path (e.g. ingest/customers.py)")],
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Run a single ingest or export script (.py or .dpnb notebook)."""
    from havn.config import load_project
    from havn.engine.database import connect
    from havn.engine.runner import run_script

    project_dir = _resolve_project(project_dir)
    config = load_project(project_dir)
    script_path = project_dir / script

    if not script_path.exists():
        console.print(f"[red]Script not found: {script_path}[/red]")
        raise typer.Exit(1)

    if script_path.is_dir():
        console.print(f"[red]Expected a script file, got a directory: {script_path}[/red]")
        console.print("Hint: use [bold]havn stream[/bold] to run a full pipeline, or specify a file like [bold]havn run ingest/script.py[/bold]")
        raise typer.Exit(1)

    # Determine script type from immediate parent directory
    parent_name = script_path.parent.name
    if parent_name == "ingest":
        script_type = "ingest"
    elif parent_name == "export":
        script_type = "export"
    else:
        script_type = "script"
    console.print(f"[bold]Running {script_type}:[/bold]")

    db_path = project_dir / config.database.path
    conn = connect(db_path)
    try:
        result = run_script(conn, script_path, script_type)
        if result["status"] == "error":
            raise typer.Exit(1)
    finally:
        conn.close()


@app.command()
def seed(
    force: Annotated[bool, typer.Option("--force", "-f", help="Force reload all seeds")] = False,
    schema: Annotated[str, typer.Option("--schema", "-s", help="Target schema for seeds")] = "seeds",
    env: Annotated[Optional[str], typer.Option("--env", "-e", help="Environment to use")] = None,
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Load CSV files from seeds/ directory into DuckDB tables.

    Seeds are change-detected: only modified CSVs are reloaded.
    Use --force to reload everything.
    """
    from havn.engine.database import connect
    from havn.engine.seeds import run_seeds

    project_dir = _resolve_project(project_dir)
    config = _load_config(project_dir, env)
    seeds_dir = project_dir / "seeds"

    if not seeds_dir.exists():
        console.print("[yellow]No seeds/ directory found.[/yellow]")
        console.print("Create a seeds/ directory with CSV files to load as seed data.")
        return

    env_label = f" [dim](env={config.active_environment})[/dim]" if config.active_environment else ""
    console.print(f"[bold]Loading seeds{env_label}:[/bold]")

    db_path = project_dir / config.database.path
    conn = connect(db_path)
    try:
        results = run_seeds(conn, seeds_dir, schema=schema, force=force)
        if results:
            built = sum(1 for s in results.values() if s == "built")
            skipped = sum(1 for s in results.values() if s == "skipped")
            errors = sum(1 for s in results.values() if s == "error")
            console.print(f"\n  {built} loaded, {skipped} skipped, {errors} errors")
            if errors:
                raise typer.Exit(1)
    finally:
        conn.close()


@app.command()
def transform(
    targets: Annotated[Optional[list[str]], typer.Argument(help="Specific models to run")] = None,
    force: Annotated[bool, typer.Option("--force", "-f", help="Force rebuild all models")] = False,
    sequential: Annotated[bool, typer.Option("--sequential", help="Disable parallel execution; run models one at a time")] = False,
    workers: Annotated[int, typer.Option("--workers", "-w", help="Max parallel workers")] = 4,
    env: Annotated[Optional[str], typer.Option("--env", "-e", help="Environment to use (e.g. dev, prod)")] = None,
    skip_check: Annotated[bool, typer.Option("--skip-check", help="Skip pre-transform validation")] = False,
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Parse SQL models, resolve DAG, execute in dependency order.

    Supports incremental models, data quality assertions, auto-profiling,
    and parallel execution of independent models.
    """
    from havn.engine.database import connect
    from havn.engine.transform import run_transform

    project_dir = _resolve_project(project_dir)
    config = _load_config(project_dir, env)
    transform_dir = project_dir / "transform"

    parallel = not sequential
    mode = "parallel" if parallel else "sequential"
    console.print(f"[bold]Transform[/bold] [dim]({mode})[/dim]:")

    db_path = project_dir / config.database.path
    conn = connect(db_path, memory_limit=config.database.memory_limit, threads=config.database.threads)

    # Schema Sentinel: check for upstream schema changes before executing
    if config.sentinel.enabled:
        try:
            from havn.engine.sentinel import (
                SentinelConfig as _SC,
                get_source_names_from_models,
                run_sentinel_check,
            )
            source_names = get_source_names_from_models(project_dir)
            # Filter to sources that actually exist in the warehouse
            existing_sources = []
            for sn in source_names:
                parts = sn.split(".")
                if len(parts) == 2:
                    try:
                        exists = conn.execute(
                            "SELECT COUNT(*) FROM information_schema.tables "
                            "WHERE table_schema = ? AND table_name = ?",
                            [parts[0], parts[1]],
                        ).fetchone()[0]
                        if exists:
                            existing_sources.append(sn)
                    except Exception:
                        pass

            if existing_sources:
                sc = _SC(
                    enabled=config.sentinel.enabled,
                    on_change=config.sentinel.on_change,
                    track_ordering=config.sentinel.track_ordering,
                    rename_inference=config.sentinel.rename_inference,
                    auto_fix=config.sentinel.auto_fix,
                    select_star_warning=config.sentinel.select_star_warning,
                )
                diffs = run_sentinel_check(project_dir, conn, existing_sources, config=sc)
                if diffs:
                    breaking = [d for d in diffs if d.has_breaking]
                    for diff in diffs:
                        n_changes = len(diff.changes)
                        severity_label = "[red]BREAKING[/red]" if diff.has_breaking else "[yellow]WARNING[/yellow]"
                        console.print(
                            f"  {severity_label}  Schema change in [bold]{diff.source_name}[/bold]: "
                            f"{n_changes} change(s)"
                        )
                        for ch in diff.changes:
                            sev = {"breaking": "[red]", "warning": "[yellow]", "info": "[dim]"}
                            sev_close = {"breaking": "[/red]", "warning": "[/yellow]", "info": "[/dim]"}
                            s = sev.get(ch.severity, "")
                            sc_ = sev_close.get(ch.severity, "")
                            detail = ""
                            if ch.old_value and ch.new_value:
                                detail = f" ({ch.old_value} -> {ch.new_value})"
                            elif ch.old_value:
                                detail = f" (was: {ch.old_value})"
                            elif ch.new_value:
                                detail = f" (type: {ch.new_value})"
                            console.print(f"           {s}{ch.change_type}{sc_}: {ch.column_name}{detail}")

                    if breaking and config.sentinel.on_change == "pause":
                        console.print(
                            "\n  [red]Pipeline paused due to breaking schema changes.[/red]"
                        )
                        console.print(
                            "  Run [bold]havn sentinel[/bold] for details, or set "
                            "[bold]sentinel.on_change: continue[/bold] in project.yml to skip."
                        )
                        conn.close()
                        raise typer.Exit(1)
        except typer.Exit:
            raise
        except Exception as e:
            logger.warning("Schema Sentinel check failed: %s", e)

    # Start a Pipeline Rewind run if enabled
    run_id = None
    if config.rewind.enabled:
        from havn.engine.snapshots import finish_run, run_gc, start_run
        run_id = start_run(project_dir, trigger="manual")

    try:
        results = run_transform(
            conn, transform_dir, targets=targets, force=force,
            parallel=parallel, max_workers=workers, db_path=str(db_path),
            project_dir=project_dir, rewind_config=config.rewind, run_id=run_id,
        )
        if not results:
            return
        built = sum(1 for s in results.values() if s == "built")
        skipped = sum(1 for s in results.values() if s == "skipped")
        errors = sum(1 for s in results.values() if s == "error")
        assertions_failed = sum(1 for s in results.values() if s == "assertion_failed")
        console.print()
        parts = [f"{built} built", f"{skipped} skipped", f"{errors} errors"]
        if assertions_failed:
            parts.append(f"{assertions_failed} assertion failures")
        console.print(f"  {', '.join(parts)}")

        # Finish the Pipeline Rewind run
        if run_id and config.rewind.enabled:
            status = "failed" if errors else ("partial" if assertions_failed else "success")
            models_run = list(results.keys())
            finish_run(project_dir, run_id, status, models_run)
            # Run garbage collection
            try:
                from havn.engine.snapshots import RewindConfig as _RC
                rw_cfg = _RC(
                    enabled=config.rewind.enabled,
                    retention=config.rewind.retention,
                    max_storage=config.rewind.max_storage,
                    dedup=config.rewind.dedup,
                    exclude=config.rewind.exclude,
                )
                deleted = run_gc(project_dir, rw_cfg)
                if deleted:
                    console.print(f"  [dim]rewind: {deleted} expired snapshot(s) cleaned up[/dim]")
            except Exception as gc_err:
                logger.warning("Snapshot GC failed: %s", gc_err)

        # Send alerts if configured
        if config.alerts.slack_webhook_url or config.alerts.webhook_url:
            from havn.engine.alerts import AlertConfig, alert_pipeline_success, alert_pipeline_failure
            alert_cfg = AlertConfig(
                slack_webhook_url=config.alerts.slack_webhook_url,
                webhook_url=config.alerts.webhook_url,
                channels=config.alerts.channels,
            )
            if errors or assertions_failed:
                if config.alerts.on_failure:
                    alert_pipeline_failure("transform", 0, f"{errors} errors, {assertions_failed} assertion failures", alert_cfg, conn)
            elif config.alerts.on_success and built > 0:
                alert_pipeline_success("transform", 0, alert_cfg, conn, models_built=built)

        if errors or assertions_failed:
            raise typer.Exit(1)
    finally:
        conn.close()


@app.command()
def stream(
    name: Annotated[str, typer.Argument(help="Stream name from project.yml")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Force rebuild all models")] = False,
    env: Annotated[Optional[str], typer.Option("--env", "-e", help="Environment to use")] = None,
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Run a full stream: ingest -> transform -> export as defined in project.yml."""
    import time as _time

    from havn.engine.database import connect
    from havn.engine.runner import run_scripts_in_dir
    from havn.engine.transform import run_transform

    project_dir = _resolve_project(project_dir)
    config = _load_config(project_dir, env)

    if name not in config.streams:
        console.print(f"[red]Stream '{name}' not found in project.yml[/red]")
        available = ", ".join(config.streams.keys()) or "(none)"
        console.print(f"Available streams: {available}")
        raise typer.Exit(1)

    stream_config = config.streams[name]
    console.print(f"[bold]Stream: {name}[/bold]")
    if stream_config.description:
        console.print(f"  {stream_config.description}")
    if stream_config.retries:
        console.print(f"  [dim]retries: {stream_config.retries}, delay: {stream_config.retry_delay}s[/dim]")
    console.print()

    db_path = project_dir / config.database.path
    conn = connect(db_path)
    has_error = False
    start = _time.perf_counter()

    # Start rewind run tracking for streams
    run_id = None
    if config.rewind.enabled:
        from havn.engine.snapshots import finish_run, run_gc, start_run
        run_id = start_run(project_dir, trigger=f"stream:{name}")

    def _run_step(step, conn_):
        """Run a single step. Returns True on success."""
        if step.action == "ingest":
            console.print("[bold]Ingest:[/bold]")
            results = run_scripts_in_dir(conn_, project_dir / "ingest", "ingest", step.targets)
            if any(r["status"] == "error" for r in results):
                return False
        elif step.action == "transform":
            console.print("[bold]Transform:[/bold]")
            results = run_transform(
                conn_,
                project_dir / "transform",
                targets=step.targets if step.targets != ["all"] else None,
                force=force,
                project_dir=project_dir,
                rewind_config=config.rewind if run_id else None,
                run_id=run_id,
            )
            if any(s == "error" for s in results.values()):
                return False
        elif step.action == "export":
            console.print("[bold]Export:[/bold]")
            results = run_scripts_in_dir(conn_, project_dir / "export", "export", step.targets)
            if any(r["status"] == "error" for r in results):
                return False
        elif step.action == "seed":
            console.print("[bold]Seeds:[/bold]")
            from havn.engine.seeds import run_seeds
            results = run_seeds(conn_, project_dir / "seeds", force=force)
            if any(s == "error" for s in results.values()):
                return False
        console.print()
        return True

    try:
        for step in stream_config.steps:
            success = _run_step(step, conn)
            if not success and stream_config.retries > 0:
                for attempt in range(1, stream_config.retries + 1):
                    console.print(
                        f"[yellow]Retrying {step.action} (attempt {attempt}/{stream_config.retries}) "
                        f"after {stream_config.retry_delay}s...[/yellow]"
                    )
                    _time.sleep(stream_config.retry_delay)
                    success = _run_step(step, conn)
                    if success:
                        break
            if not success:
                has_error = True
                break

        duration_s = round(_time.perf_counter() - start, 1)
        if has_error:
            console.print(f"[red]Stream failed after {duration_s}s.[/red]")
        else:
            console.print(f"[green]Stream completed successfully in {duration_s}s.[/green]")

        # Finish rewind run
        if run_id and config.rewind.enabled:
            status = "failed" if has_error else "success"
            models_run = []  # populated by transform step internally
            finish_run(project_dir, run_id, status, models_run)
            try:
                from havn.engine.snapshots import RewindConfig as _RC
                rw_cfg = _RC(
                    enabled=config.rewind.enabled,
                    retention=config.rewind.retention,
                    max_storage=config.rewind.max_storage,
                    dedup=config.rewind.dedup,
                    exclude=config.rewind.exclude,
                )
                deleted = run_gc(project_dir, rw_cfg)
                if deleted:
                    console.print(f"  [dim]rewind: {deleted} expired snapshot(s) cleaned up[/dim]")
            except Exception:
                pass

        # Webhook notification
        if stream_config.webhook_url:
            _send_webhook(stream_config.webhook_url, name, "failed" if has_error else "success", duration_s)

        if has_error:
            raise typer.Exit(1)
    finally:
        conn.close()


def _send_webhook(url: str, stream_name: str, status: str, duration_s: float) -> None:
    """Send a POST webhook notification for stream completion."""
    import json
    from datetime import datetime
    from urllib.request import Request, urlopen

    payload = json.dumps({
        "stream": stream_name,
        "status": status,
        "duration_seconds": duration_s,
        "timestamp": datetime.now().isoformat(),
    }).encode()

    try:
        req = Request(url, data=payload, headers={"Content-Type": "application/json"})
        urlopen(req, timeout=10)
        console.print(f"[dim]Webhook sent to {url}[/dim]")
    except Exception as e:
        console.print(f"[yellow]Webhook failed: {e}[/yellow]")


@app.command()
def lint(
    fix: Annotated[bool, typer.Option("--fix", help="Auto-fix violations")] = False,
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Lint SQL files in the transform directory with SQLFluff."""
    from havn.config import load_project
    from havn.lint.linter import lint as run_lint
    from havn.lint.linter import print_violations

    project_dir = _resolve_project(project_dir)
    config = load_project(project_dir)
    transform_dir = project_dir / "transform"

    action = "Fixing" if fix else "Linting"
    console.print(f"[bold]{action} SQL files...[/bold]")

    count, violations, fixed = run_lint(
        transform_dir,
        fix=fix,
        dialect=config.lint.dialect,
        rules=config.lint.rules or None,
    )

    print_violations(violations)

    if fix:
        if fixed > 0:
            console.print(f"[green]{fixed} fixed.[/green]")
        if count > 0:
            console.print(f"[yellow]{count} violation(s) remain (unfixable by SQLFluff).[/yellow]")
            raise typer.Exit(1)
        if fixed == 0 and count == 0:
            console.print("[green]All clean — no violations found.[/green]")
    elif count > 0:
        console.print(f"\n[red]{count} violation(s) found.[/red] Run [bold]havn lint --fix[/bold] to auto-fix.")
        raise typer.Exit(1)
    else:
        console.print("[green]All clean — no violations found.[/green]")


@app.command()
def watch(
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Watch for file changes and auto-rebuild transforms."""
    from havn.engine.scheduler import FileWatcher

    project_dir = _resolve_project(project_dir)
    console.print("[bold]Watching for changes...[/bold] (Ctrl+C to stop)")
    console.print(f"  transform/  -> auto-rebuild SQL models")

    watcher = FileWatcher(project_dir)
    watcher.start()

    try:
        watcher.join()
    except KeyboardInterrupt:
        watcher.stop()
        console.print("\n[dim]Watcher stopped.[/dim]")


@app.command()
def schedule(
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory (default: current dir)")] = None,
) -> None:
    """Show scheduled streams and start the scheduler."""
    from havn.engine.scheduler import SchedulerThread, get_scheduled_streams

    project_dir = _resolve_project(project_dir)
    scheduled = get_scheduled_streams(project_dir)

    if not scheduled:
        console.print("[yellow]No scheduled streams found in project.yml[/yellow]")
        console.print("Add a schedule to a stream:")
        console.print('  schedule: "0 6 * * *"  # 6am daily')
        return

    table = Table(title="Scheduled Streams")
    table.add_column("Stream", style="bold")
    table.add_column("Schedule")
    table.add_column("Description")
    for s in scheduled:
        table.add_row(s["name"], s["schedule"], s["description"])
    console.print(table)
    console.print()

    console.print("[bold]Starting scheduler...[/bold] (Ctrl+C to stop)")
    scheduler = SchedulerThread(project_dir)
    scheduler.start()

    try:
        scheduler.join()
    except KeyboardInterrupt:
        scheduler.stop()
        console.print("\n[dim]Scheduler stopped.[/dim]")


# --- cdc ---


@app.command()
def cdc(
    action: Annotated[str, typer.Argument(help="Action: status, reset")] = "status",
    connector: Annotated[Optional[str], typer.Option("--connector", "-c", help="Connector name")] = None,
    table_name: Annotated[Optional[str], typer.Option("--table", "-t", help="Table name (for reset)")] = None,
    project_dir: Annotated[Optional[Path], typer.Option("--project", "-p", help="Project directory")] = None,
) -> None:
    """View and manage CDC (Change Data Capture) state.

    Examples:
      havn cdc status                         # Show all CDC state
      havn cdc status --connector prod_users  # Show state for one connector
      havn cdc reset --connector prod_users   # Reset all watermarks for a connector
      havn cdc reset --connector prod --table users  # Reset one table
    """
    from havn.config import load_project
    from havn.engine.cdc import get_cdc_status, reset_watermark
    from havn.engine.database import connect

    project_dir = _resolve_project(project_dir)
    config = load_project(project_dir)
    db_path = project_dir / config.database.path

    if not db_path.exists():
        console.print("[yellow]No warehouse database found. Run a pipeline first.[/yellow]")
        raise typer.Exit(1)

    conn = connect(db_path)
    try:
        if action == "status":
            entries = get_cdc_status(conn, connector)
            if not entries:
                console.print("[yellow]No CDC state tracked yet.[/yellow]")
                return
            tbl = Table(title="CDC State")
            tbl.add_column("Connector", style="bold")
            tbl.add_column("Table")
            tbl.add_column("Mode")
            tbl.add_column("Watermark")
            tbl.add_column("Last Sync")
            tbl.add_column("Rows", justify="right")
            for e in entries:
                tbl.add_row(
                    e["connector"],
                    e["table"],
                    e["cdc_mode"],
                    e["watermark"] or "-",
                    e["last_sync_at"][:19] if e["last_sync_at"] else "-",
                    str(e["rows_synced"]),
                )
            console.print(tbl)

        elif action == "reset":
            if not connector:
                console.print("[red]--connector is required for reset[/red]")
                raise typer.Exit(1)
            reset_watermark(conn, connector, table_name)
            target = f"{connector}.{table_name}" if table_name else connector
            console.print(f"[green]CDC state reset for {target}[/green]")

        else:
            console.print(f"[red]Unknown action: {action}[/red]")
            console.print("Available: status, reset")
            raise typer.Exit(1)
    finally:
        conn.close()
