"""
Engine Bridge for Async API - SaaS variant
===========================================
- Async wrappers around sync engine calls
- Robust progress: sentinel shutdown, FINAL on errors
- Returns: (pandas.DataFrame, RunStats) to match Jobs v2 usage
"""

import asyncio
import logging
import os
import queue
import time
from concurrent.futures import ThreadPoolExecutor
from typing import Callable, Awaitable, Optional, Dict, Any, Union, Tuple

import pandas as pd

# Re-exported names so jobs.py can import them from this module
from fmatch.core.progress_event import ProgressPhase, ProgressEvent
from fmatch.core.engine import (
    process_dataframe,
    make_blocking_column,
    RunStats,
    determine_optimal_blocking_strategy,
    BlockingMode,
    DedupeConfig,
    MatchConfig,
)

log = logging.getLogger(__name__)

# Single worker — use K8s horizontal scale for throughput
def _resolve_engine_bridge_workers() -> int:
    raw_value = os.getenv("FM_ENGINE_BRIDGE_MAX_WORKERS", "").strip()
    if raw_value:
        try:
            return max(1, min(int(raw_value), 8))
        except ValueError:
            log.warning(
                "Invalid FM_ENGINE_BRIDGE_MAX_WORKERS=%r; falling back to default",
                raw_value,
            )

    cpu_count = os.cpu_count() or 1
    if cpu_count <= 1:
        return 1
    return min(2, cpu_count)


_executor = ThreadPoolExecutor(max_workers=_resolve_engine_bridge_workers())


class _ProgressMonitor:
    """
    Pulls ProgressEvent objects from a queue and forwards them to an async callback.
    Stops cleanly when it receives a sentinel (None).
    """

    def __init__(
        self,
        q: queue.Queue,
        callback: Callable[[ProgressEvent], Awaitable[None]],
        total_rows: int = 0,
    ):
        self.q = q
        self.callback = callback
        self.total_rows = total_rows

    async def run(self, loop: asyncio.AbstractEventLoop) -> None:
        log.info("Progress monitor starting.")
        try:
            while True:
                try:
                    # blocking get in a worker thread to avoid blocking the event loop
                    ev = await loop.run_in_executor(
                        None, lambda: self.q.get(timeout=0.25)
                    )
                except queue.Empty:
                    continue

                if ev is None:
                    log.info("Progress monitor sentinel received. Exiting.")
                    break

                if isinstance(ev, ProgressEvent):
                    await self.callback(ev)
                else:
                    log.warning(f"Unexpected progress message: {type(ev)}")
        finally:
            log.info("Progress monitor exited.")


async def _run_engine(
    loop: asyncio.AbstractEventLoop,
    cfg: Union[DedupeConfig, MatchConfig],
    src_df_processed: pd.DataFrame,
    ref_df_processed: Optional[pd.DataFrame],
    id_args: Dict[str, Any],
    progress_q: queue.Queue,
) -> Tuple[pd.DataFrame, RunStats]:
    """
    Run the synchronous engine function in a background thread and return (result_df, run_stats).
    Ensures a FINAL event is emitted on exceptions so clients never hang.
    """
    total_rows = len(src_df_processed)
    run_stats = RunStats(
        total_items_to_process=total_rows,
        emit_interval_s=0.1,
        job_id=getattr(cfg, "job_id", None),
    )
    # Track start time for duration calculation
    run_stats._start_time = time.time()
    # Allow the core to emit ProgressEvents to our queue
    run_stats._last_progress_q = progress_q  # relies on core honoring this

    try:
        result_df = await loop.run_in_executor(
            _executor,
            process_dataframe,
            src_df_processed,
            ref_df_processed,
            cfg,
            id_args.get("rec_id_col"),
            id_args.get("src_id_col"),
            id_args.get("ref_id_col"),
            progress_q,
            run_stats,
        )
        return result_df, run_stats
    except Exception as e:
        log.error(f"Engine execution error: {e}", exc_info=True)

        # Create a proper FINAL event with timestamp
        final = ProgressEvent(
            phase=ProgressPhase.FINAL.value,
            message=f"Error: {e}",
            total_items=total_rows,
            timestamp_ns=time.perf_counter_ns(),
            extras={"op_id": getattr(cfg, "job_id", None), "error": str(e)},
        )
        # Unblock the monitor/UI
        progress_q.put(final)

        # Fix: Use duration instead of total_duration
        start_time = getattr(run_stats, "_start_time", time.time())
        if hasattr(run_stats, "duration"):
            run_stats.duration = time.time() - start_time
        else:
            # Create the attribute if it doesn't exist
            setattr(run_stats, "duration", time.time() - start_time)

        # Set error state
        run_stats.phase = "error"
        run_stats.error = str(e)

        raise


async def dedupe(
    cfg: DedupeConfig,
    progress_callback: Optional[Callable[[ProgressEvent], Awaitable[None]]] = None,
) -> Tuple[pd.DataFrame, RunStats]:
    """
    Async wrapper for deduplication.
    Returns: (results_df, run_stats)
    """
    loop = asyncio.get_running_loop()

    # Load data if path provided
    if isinstance(cfg.input_path, pd.DataFrame):
        df_in = cfg.input_path
    else:
        df_in = pd.read_csv(cfg.input_path)

    # Apply blocking if configured
    df_proc = (
        make_blocking_column(df=df_in, cfg=cfg, df_type="source")
        if cfg.apply_blocking
        else df_in
    )

    # Create progress queue and monitor
    progress_q = queue.Queue()
    monitor_task = None

    if progress_callback:
        mon = _ProgressMonitor(progress_q, progress_callback, total_rows=len(df_proc))
        monitor_task = asyncio.create_task(mon.run(loop))

        # Initial event so UI lights up immediately
        await progress_callback(
            ProgressEvent(
                phase=ProgressPhase.INIT.value,
                message="Initializing deduplication...",
                total_items=len(df_proc),
                timestamp_ns=time.perf_counter_ns(),
            )
        )

    try:
        result_df, stats = await _run_engine(
            loop=loop,
            cfg=cfg,
            src_df_processed=df_proc,
            ref_df_processed=None,
            id_args={"rec_id_col": cfg.rec_id_col},
            progress_q=progress_q,
        )
        return result_df, stats
    finally:
        if monitor_task:
            progress_q.put(None)  # sentinel
            await monitor_task


async def match(
    cfg: MatchConfig,
    progress_callback: Optional[Callable[[ProgressEvent], Awaitable[None]]] = None,
) -> Tuple[pd.DataFrame, RunStats]:
    """
    Async wrapper for matching.
    Returns: (results_df, run_stats)
    """
    loop = asyncio.get_running_loop()

    # Load source and reference data
    src_df = (
        cfg.src_path
        if isinstance(cfg.src_path, pd.DataFrame)
        else pd.read_csv(cfg.src_path)
    )
    ref_df = (
        cfg.ref_path
        if isinstance(cfg.ref_path, pd.DataFrame)
        else pd.read_csv(cfg.ref_path)
    )

    # Apply blocking if configured
    if cfg.apply_blocking:
        src_proc = make_blocking_column(df=src_df, cfg=cfg, df_type="source")
        ref_proc = make_blocking_column(df=ref_df, cfg=cfg, df_type="reference")
    else:
        src_proc, ref_proc = src_df, ref_df

    # Create progress queue and monitor
    progress_q = queue.Queue()
    monitor_task = None

    if progress_callback:
        mon = _ProgressMonitor(progress_q, progress_callback, total_rows=len(src_proc))
        monitor_task = asyncio.create_task(mon.run(loop))

        # Initial event
        await progress_callback(
            ProgressEvent(
                phase=ProgressPhase.INIT.value,
                message="Initializing matching...",
                total_items=len(src_proc),
                timestamp_ns=time.perf_counter_ns(),
            )
        )

    try:
        result_df, stats = await _run_engine(
            loop=loop,
            cfg=cfg,
            src_df_processed=src_proc,
            ref_df_processed=ref_proc,
            id_args={"src_id_col": cfg.src_id_col, "ref_id_col": cfg.ref_id_col},
            progress_q=progress_q,
        )
        # Note: Deduplication for best_match_only is now handled in jobs.py
        return result_df, stats
    finally:
        if monitor_task:
            progress_q.put(None)  # sentinel
            await monitor_task


async def suggest_blocking(
    src_df: pd.DataFrame,
    ref_df: Optional[pd.DataFrame] = None,
    mappings: Optional[list] = None,
    mode: str = "dedupe",
    ruleset: str = "default",
) -> Dict[str, Any]:
    """
    Async wrapper around determine_optimal_blocking_strategy.
    """
    loop = asyncio.get_running_loop()
    blocking_mode = BlockingMode.DEDUPE if mode == "dedupe" else BlockingMode.MATCH

    return await loop.run_in_executor(
        None,
        determine_optimal_blocking_strategy,
        src_df,
        ref_df,
        mappings or [],
        blocking_mode,
        ruleset,
    )


def shutdown_executor():
    """Cleanly shut down the thread pool executor."""
    _executor.shutdown(wait=True)


# Re-export configs for convenience
__all__ = [
    "dedupe",
    "match",
    "suggest_blocking",
    "DedupeConfig",
    "MatchConfig",
    "ProgressEvent",
    "ProgressPhase",
    "RunStats",
    "shutdown_executor",
]
