"""
Route handlers for ETL pipeline execution.

Allows running an ETL pipeline from YAML config strings (extract, transform, load).
"""

from __future__ import annotations

import logging
import uuid
from typing import Any

import yaml
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from pycharter import Pipeline
from pycharter.api.dependencies.database import get_db_session
from pycharter.api.models.etl import (
    EtlRunRequest,
    EtlRunResponse,
    pipeline_quality_report_from_dict,
)
from pycharter.db.models.pipeline_run import PipelineRunModel

logger = logging.getLogger(__name__)

router = APIRouter(tags=["ETL"])


def _parse_yaml_config(
    yaml_str: str, field_name: str
) -> dict[str, Any] | list[dict[str, Any]]:
    """
    Parse a YAML string into a dict or list of dicts.
    Empty or whitespace-only string returns {}.
    Raises HTTPException 400 on parse error or invalid type.
    """
    if not yaml_str or not yaml_str.strip():
        return {}
    try:
        parsed = yaml.safe_load(yaml_str)
    except yaml.YAMLError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid YAML in {field_name}: {str(e)}",
        )
    if parsed is None:
        return {}
    if isinstance(parsed, dict):
        return parsed
    if isinstance(parsed, list):
        if all(isinstance(item, dict) for item in parsed):
            return parsed
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"{field_name} must be a YAML object or list of objects",
        )
    raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=f"{field_name} must be a YAML object or list of objects, got {type(parsed).__name__}",
    )


@router.post(
    "/etl/run",
    response_model=EtlRunResponse,
    status_code=status.HTTP_200_OK,
    summary="Run ETL pipeline",
    description="Run an ETL pipeline from extract, transform, and load YAML config strings. Optional variables are applied for ${VAR} substitution. Use dry_run=True to extract and transform without loading. When load config includes quality_checks, column/dataset-based checks (row count, null rate, uniqueness, etc.) run after load; results appear in pipeline_quality_report. For row-based (contract) quality, use POST /quality/check.",
    response_description="Pipeline run result with row counts, optional pipeline_quality_report, and any errors",
    tags=["ETL"],
)
async def run_etl_pipeline(
    request: EtlRunRequest,
    db: Session = Depends(get_db_session),
) -> EtlRunResponse:
    """
    Run an ETL pipeline from the provided YAML configs.

    Parses extract_yaml, load_yaml, and optional transform_yaml; builds a pipeline
    via Pipeline.from_dict() (so variable substitution is applied), runs it, and
    returns the result (row counts, success, errors).
    """
    # Parse YAML configs
    extract_dict = _parse_yaml_config(request.extract_yaml, "extract")
    load_dict = _parse_yaml_config(request.load_yaml, "load")
    transform_raw = _parse_yaml_config(request.transform_yaml or "", "transform")

    if not isinstance(extract_dict, dict) or not extract_dict:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Extract config is required and must be a non-empty YAML object",
        )
    if not isinstance(load_dict, dict) or not load_dict:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Load config is required and must be a non-empty YAML object",
        )

    # Build config for Pipeline.from_dict
    # The transform YAML can contain: transform (simple ops), jsonata, custom_function
    # We pass the full parsed dict to from_dict which will extract these
    config: dict[str, Any] = {
        "extract": extract_dict,
        "load": load_dict,
    }

    # If transform_raw is a dict, include its keys in config (transform, jsonata, custom_function)
    if isinstance(transform_raw, dict):
        if "transform" in transform_raw:
            config["transform"] = transform_raw["transform"]
        if "jsonata" in transform_raw:
            config["jsonata"] = transform_raw["jsonata"]
        if "custom_function" in transform_raw:
            config["custom_function"] = transform_raw["custom_function"]
    elif isinstance(transform_raw, list):
        # If it's a list, treat it as transform steps
        config["transform"] = transform_raw

    if request.settings_yaml and request.settings_yaml.strip():
        settings_parsed = _parse_yaml_config(request.settings_yaml, "settings")
        if isinstance(settings_parsed, dict):
            for key in ("metadata_store", "dlq", "contract", "lineage"):
                if key in settings_parsed:
                    config[key] = settings_parsed[key]

    if request.variables:
        config["variables"] = request.variables

    try:
        pipeline = Pipeline.from_dict(config)
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(e),
        )
    except Exception as e:
        logger.exception("Pipeline build failed")
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Pipeline config invalid: {str(e)}",
        )

    try:
        result = await pipeline.run(dry_run=request.dry_run)
    except Exception as e:
        logger.exception("Pipeline run failed")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=str(e),
        )

    # Persist run to pipeline_runs table for dashboard analytics
    _save_pipeline_run(db, result, extract_dict, load_dict, request.dry_run)

    return _pipeline_result_to_etl_response(result)


def _pipeline_result_to_etl_response(result: Any) -> EtlRunResponse:
    """Build EtlRunResponse from PipelineResult (explicit mapping, no to_dict)."""
    pipeline_quality_report = None
    if result.quality_report is not None:
        pipeline_quality_report = pipeline_quality_report_from_dict(
            result.quality_report.to_dict()
        )
    return EtlRunResponse(
        success=result.success,
        rows_extracted=result.rows_extracted,
        rows_transformed=result.rows_transformed,
        rows_loaded=result.rows_loaded,
        rows_failed=result.rows_failed,
        rows_quarantined_extract=result.rows_quarantined_extract,
        rows_quarantined_load=result.rows_quarantined_load,
        total_quarantined=result.total_quarantined,
        validation_errors_extract=result.validation_errors_extract or [],
        validation_errors_load=result.validation_errors_load or [],
        duration_seconds=result.duration_seconds,
        batches_processed=result.batches_processed,
        errors=result.errors or [],
        pipeline_name=result.pipeline_name,
        run_id=result.run_id,
        lineage_events=result.lineage_events or [],
        pipeline_quality_report=pipeline_quality_report,
    )


def _save_pipeline_run(
    db: Session,
    result: Any,
    extract_config: dict[str, Any],
    load_config: dict[str, Any],
    dry_run: bool,
) -> None:
    """Persist a PipelineResult to the pipeline_runs table.

    Failures are logged but never propagated — the ETL result is always returned.
    """
    try:
        run_id = result.run_id or uuid.uuid4().hex[:8]

        # Determine status
        if result.success:
            run_status = "success"
        elif result.rows_loaded > 0:
            run_status = "partial"
        else:
            run_status = "failed"

        # Pipeline (column/dataset) quality: ETL quality_checks result
        pipeline_quality_passed = None
        if result.quality_report is not None:
            pipeline_quality_passed = getattr(result.quality_report, "passed", None)

        record = PipelineRunModel(
            id=uuid.uuid4(),
            run_id=run_id,
            pipeline_name=result.pipeline_name,
            status=run_status,
            rows_extracted=result.rows_extracted,
            rows_transformed=result.rows_transformed,
            rows_loaded=result.rows_loaded,
            rows_failed=result.rows_failed,
            rows_quarantined=result.total_quarantined,
            batches_processed=result.batches_processed,
            started_at=result.start_time,
            completed_at=result.end_time,
            duration_seconds=result.duration_seconds,
            contract_quality_score=None,
            contract_quality_passed=None,
            pipeline_quality_passed=pipeline_quality_passed,
            extractor_type=extract_config.get("type"),
            loader_type=load_config.get("type"),
            trigger="dry_run" if dry_run else "api",
            errors=result.errors if result.errors else None,
            batch_results=(
                [
                    {
                        "batch_index": br.batch_index,
                        "rows_in": br.rows_in,
                        "rows_out": br.rows_out,
                        "rows_failed": br.rows_failed,
                        "errors": br.errors,
                    }
                    for br in result.batch_results
                ]
                if result.batch_results
                else None
            ),
            config_snapshot={
                "extract": extract_config,
                "load": load_config,
            },
        )
        db.add(record)
        db.commit()
        logger.info("Saved pipeline run %s to dashboard", run_id)
    except Exception:
        logger.warning("Failed to save pipeline run to dashboard", exc_info=True)
        try:
            db.rollback()
        except Exception:
            pass
