# In engine_bridge.py, replace the entire file content.

"""
Engine Bridge for Async GUI - Modern Progress System
====================================================
Provides async wrappers around the synchronous engine functions with enhanced progress reporting.
Uses the modern ProgressEvent format exclusively for consistent progress tracking.

FIXED VERSION: Includes all deadlock and progress queue fixes.
"""

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

from fmatch.core.progress_event import ProgressPhase, ProgressEvent

from fmatch.core import DedupeConfig, MatchConfig, determine_optimal_blocking_strategy
from fmatch.core.engine import (
    process_dataframe,
    make_blocking_column,
    RunStats,
)
from fmatch.desktop.models import (
    BlockingSuggestionModel,
    DuplicateResultModel,
    MatchResultModel,
)

import pandas as pd
import os
from pathlib import Path
import copy

# Use a single thread pool executor to avoid creating new threads for each call
executor = ThreadPoolExecutor(max_workers=1)
log = logging.getLogger(__name__)


def _read_csv_robust(
    path,
    *,
    encoding=None,
    engine=None,
    delimiter=None,
    quotechar=None,
    escapechar=None,
    has_header=True,
    nrows=None,
):
    """
    Read CSV with robust encoding fallback and smart engine selection.
    Use C engine when delimiter is known (faster), Python for auto-detection.
    """

    # Smart engine selection
    # - Use C engine if delimiter is known (faster, supports low_memory)
    # - Use Python engine for delimiter sniffing
    # - Respect explicit engine if provided
    eng = engine or ("c" if delimiter else "python")

    # Build kwargs
    kw = {
        "engine": eng,
        "sep": delimiter,  # None => auto-detect with Python engine
        "quotechar": quotechar,
        "escapechar": escapechar,
        "header": 0 if (has_header is not False) else None,
        "nrows": nrows,
        "on_bad_lines": "warn",  # Don't crash on malformed lines
    }

    # Only C engine supports low_memory
    if eng == "c":
        kw["low_memory"] = False  # Better dtype inference

    # Remove None values
    kw = {k: v for k, v in kw.items() if v is not None}

    # Try encodings in order
    encodings_to_try = [encoding] if encoding else []
    encodings_to_try.extend(["utf-8", "utf-8-sig", "cp1252", "latin-1", "iso-8859-1"])
    encodings_to_try = list(dict.fromkeys(encodings_to_try))  # Remove duplicates

    tried, last_error = [], None

    for enc in encodings_to_try:
        if not enc:
            continue
        try:
            df = pd.read_csv(path, encoding=enc, **kw)
            if enc != encoding and encoding:
                log.info(
                    f"Read {Path(path).name} using fallback encoding '{enc}' (requested: '{encoding}')"
                )
            return df
        except UnicodeDecodeError as e:
            tried.append(enc)
            last_error = e
        except ValueError as e:
            # Safety: if low_memory somehow got through to Python engine
            if "low_memory" in str(e).lower() and eng == "python":
                kw.pop("low_memory", None)
                try:
                    return pd.read_csv(path, encoding=enc, **kw)
                except Exception as e2:
                    last_error = e2
            else:
                last_error = e
            tried.append(enc)
        except Exception as e:
            log.warning(f"Error reading {Path(path).name} with {enc}: {e}")
            tried.append(enc)
            last_error = e

    # Last resort: latin-1 with error replacement
    log.warning(f"All encodings failed ({tried}); using latin-1 with error replacement")
    try:
        # Ensure no low_memory for last resort
        final_kw = {k: v for k, v in kw.items() if k != "low_memory"}
        return pd.read_csv(
            path, encoding="latin-1", encoding_errors="replace", **final_kw
        )
    except Exception as e:
        log.error(f"Failed to read {Path(path).name} even with error replacement: {e}")
        raise last_error or e


def _read_table_from_path(path, cfg, *, nrows=None):
    """Read any supported table format based on extension and config."""
    path_str = str(path)
    ext = os.path.splitext(path_str)[-1].lower()

    # Excel files
    if ext in (".xls", ".xlsx", ".xlsm", ".xlsb"):
        try:
            sheet_name = getattr(cfg, "sheet_name", 0)  # Default to first sheet
            return pd.read_excel(path, sheet_name=sheet_name, nrows=nrows)
        except Exception as e:
            log.error(f"Failed to read Excel file {Path(path).name}: {e}")
            raise

    # Parquet files (if you ever support them)
    if ext == ".parquet":
        try:
            if nrows:
                return pd.read_parquet(path).head(nrows)
            return pd.read_parquet(path)
        except Exception as e:
            log.error(f"Failed to read Parquet file {Path(path).name}: {e}")
            raise

    # Default to CSV/TSV
    return _read_csv_robust(
        path,
        encoding=getattr(cfg, "encoding", None),
        engine=getattr(cfg, "csv_engine", None),  # Let _read_csv_robust decide
        delimiter=getattr(cfg, "delimiter", None),
        quotechar=getattr(cfg, "quotechar", None),
        escapechar=getattr(cfg, "escapechar", None),
        has_header=getattr(cfg, "has_header", True),
        nrows=nrows,
    )


class ProgressMonitor:
    """
    Monitors a progress queue and calls an async callback with ProgressEvent objects.
    This version passes events directly without translation.
    """

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

    async def monitor(self, loop: asyncio.AbstractEventLoop) -> None:
        """Main monitoring loop."""
        log.info("Progress monitor starting.")
        while True:
            try:
                # Use a small timeout to allow the loop to remain responsive
                event = await loop.run_in_executor(
                    None, lambda: self.queue.get(timeout=0.25)
                )

                if event is None:  # Sentinel value to stop the monitor
                    log.info("Progress monitor received sentinel. Stopping.")
                    break

                if isinstance(event, ProgressEvent):
                    await self.callback(event)
                else:
                    log.warning(
                        f"ProgressMonitor received unexpected message type: {type(event)}"
                    )

            except queue.Empty:
                # Timeout occurred, loop continues
                continue
            except Exception as e:
                log.error(f"Error in progress monitor loop: {e}", exc_info=True)
                # Avoid getting stuck in a fast error loop
                await asyncio.sleep(1)
        log.info("Progress monitor exited normally.")


async def _execute_engine_run(
    loop: asyncio.AbstractEventLoop,
    cfg: Union[DedupeConfig, MatchConfig],
    src_df_processed: pd.DataFrame,
    ref_df_processed: Optional[pd.DataFrame],
    id_args: Dict[str, Any],
    result_model: Union[Type[DuplicateResultModel], Type[MatchResultModel]],
    progress_queue: queue.Queue,
) -> Union[DuplicateResultModel, MatchResultModel]:
    """
    A single, consolidated helper to run the core engine process and handle results.
    """
    total_rows = len(src_df_processed)
    start_time = time.time()

    try:
        # Create RunStats, ensuring the job_id is passed for final event identification
        run_stats = RunStats(
            total_items_to_process=total_rows, emit_interval_s=0.1, job_id=cfg.job_id
        )
        # The RunStats object will use this queue to emit ProgressEvents
        run_stats._last_progress_q = progress_queue

        # Run the synchronous, CPU-bound engine function in a thread pool
        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_queue,
            run_stats,
        )

        matches = (
            result_df.to_dict("records")
            if result_df is not None and not result_df.empty
            else []
        )
        return result_model(matches=matches, total_duration=time.time() - start_time)

    except Exception as e:
        log.error(f"Error in engine execution bridge: {e}", exc_info=True)
        # In case of an error, create a final error event to unblock the UI
        error_event = ProgressEvent(
            phase=ProgressPhase.FINAL.value,
            message=f"Error: {e}",
            total_items=total_rows,
            extras={"op_id": cfg.job_id, "error": str(e)},
        )
        progress_queue.put(error_event)
        return result_model(matches=[], total_duration=time.time() - start_time)


async def dedupe(
    cfg: DedupeConfig, progress_callback: Callable[[ProgressEvent], Awaitable[None]]
) -> DuplicateResultModel:
    """Async wrapper for deduplication."""
    loop = asyncio.get_running_loop()

    # Use robust reader for input
    input_df = (
        cfg.input_path
        if isinstance(cfg.input_path, pd.DataFrame)
        else _read_table_from_path(cfg.input_path, cfg)
    )
    df_processed = (
        make_blocking_column(df=input_df, cfg=cfg, df_type="source")
        if cfg.apply_blocking
        else input_df
    )
    id_args = {"rec_id_col": cfg.rec_id_col}
    total_rows = len(df_processed)

    progress_queue = queue.Queue()
    monitor = ProgressMonitor(progress_queue, progress_callback, total_rows)
    monitor_task = asyncio.create_task(monitor.monitor(loop))

    try:
        # Send an initial event to the UI
        initial_event = ProgressEvent(
            phase=ProgressPhase.INIT.value,
            message="Initializing deduplication...",
            total_items=total_rows,
        )
        await progress_callback(initial_event)

        result = await _execute_engine_run(
            loop=loop,
            cfg=cfg,
            src_df_processed=df_processed,
            ref_df_processed=None,
            id_args=id_args,
            result_model=DuplicateResultModel,
            progress_queue=progress_queue,
        )
        return result
    finally:
        log.debug("Shutting down progress monitor for dedupe operation.")
        progress_queue.put(None)  # Send sentinel to stop monitor
        await monitor_task  # Wait for monitor to finish cleanly


async def match(
    cfg: MatchConfig, progress_callback: Callable[[ProgressEvent], Awaitable[None]]
) -> MatchResultModel:
    """Async wrapper for matching."""
    loop = asyncio.get_running_loop()

    # Use robust reader for source
    src_df = (
        cfg.src_path
        if isinstance(cfg.src_path, pd.DataFrame)
        else _read_table_from_path(cfg.src_path, cfg)
    )

    # Handle separate ref encoding if provided
    ref_cfg = cfg  # Use same config by default
    if hasattr(cfg, "ref_encoding") and cfg.ref_encoding:
        # Create a copy with ref_encoding as encoding
        ref_cfg = copy.copy(cfg)
        ref_cfg.encoding = cfg.ref_encoding

    # Use robust reader for reference
    ref_df = (
        cfg.ref_path
        if isinstance(cfg.ref_path, pd.DataFrame)
        else _read_table_from_path(cfg.ref_path, ref_cfg)
    )

    if cfg.apply_blocking:
        src_df_processed = make_blocking_column(df=src_df, cfg=cfg, df_type="source")
        ref_df_processed = make_blocking_column(df=ref_df, cfg=cfg, df_type="reference")
    else:
        src_df_processed = src_df
        ref_df_processed = ref_df

    id_args = {"src_id_col": cfg.src_id_col, "ref_id_col": cfg.ref_id_col}
    total_rows = len(src_df_processed)

    progress_queue = queue.Queue()
    monitor = ProgressMonitor(progress_queue, progress_callback, total_rows)
    monitor_task = asyncio.create_task(monitor.monitor(loop))

    try:
        # Send an initial event to the UI
        initial_event = ProgressEvent(
            phase=ProgressPhase.INIT.value,
            message="Initializing matching...",
            total_items=total_rows,
        )
        await progress_callback(initial_event)

        result = await _execute_engine_run(
            loop=loop,
            cfg=cfg,
            src_df_processed=src_df_processed,
            ref_df_processed=ref_df_processed,
            id_args=id_args,
            result_model=MatchResultModel,
            progress_queue=progress_queue,
        )
        return result
    finally:
        log.debug("Shutting down progress monitor for match operation.")
        progress_queue.put(None)  # Send sentinel to stop monitor
        await monitor_task  # Wait for monitor to finish cleanly


async def suggest_blocking(
    src_df: pd.DataFrame,
    ref_df: Optional[pd.DataFrame] = None,
    mappings: list = None,
    mode: str = "dedupe",
    ruleset: str = "default",
) -> BlockingSuggestionModel:
    """Async wrapper for the core blocking suggestion function."""
    from fmatch.core.engine import BlockingMode

    loop = asyncio.get_running_loop()
    blocking_mode = BlockingMode.DEDUPE if mode == "duplicate" else BlockingMode.MATCH

    result = await loop.run_in_executor(
        executor,
        determine_optimal_blocking_strategy,
        src_df,
        ref_df,
        mappings,
        blocking_mode,
        ruleset,
    )
    return BlockingSuggestionModel(
        column=result.get("source_block_col", ""),
        blocked_pairs=result.get("blocked_pairs", 0),
        total_pairs=result.get("total_pairs", 0),
    )


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