"""
Fuzzy matching helpers for g-gremlin.
"""

from __future__ import annotations

import csv
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING

logger = logging.getLogger(__name__)

LOCAL_MATCH_MAX_ROWS = 500

if TYPE_CHECKING:
    from fmatch.saas.services.match_gateway import GatewayUnavailableResult


@dataclass
class FuzzyMatchSummary:
    """
    Summary of a fuzzy match run.
    """

    matched: int
    low_confidence: int
    no_match: int
    total: int


def run_fuzzy_match(
    source_path: Path,
    source_col: str,
    target_path: Path,
    target_col: str,
    output_path: Path,
    threshold: float = 0.8,
    suffixes: Optional[List[str]] = None,
) -> Union[FuzzyMatchSummary, "GatewayUnavailableResult"]:
    """
    Fuzzy-match names from source CSV to target CSV.

    Local fuzzy matching is disabled; requires MatchGateway.
    """
    source_rows, _ = _read_csv(source_path)
    return _gateway_unavailable_result(
        source_rows,
        "Local fuzzy matching is disabled. Use MatchGateway.",
    )


async def run_fuzzy_match_engine(
    source_path: Path,
    source_col: str,
    target_path: Path,
    target_col: str,
    output_path: Path,
    threshold: float = 0.8,
    suffixes: Optional[List[str]] = None,
    engine: Optional[Any] = None,
    intel: Optional[Any] = None,
) -> Union[FuzzyMatchSummary, "GatewayUnavailableResult"]:
    """
    Enhanced fuzzy-match using MatchGateway when available.
    """
    try:
        from fmatch.saas.services.match_gateway import MatchGateway, GatewayUnavailableResult
    except ImportError:
        logger.warning("MatchGateway unavailable; local fuzzy matching is disabled.")
        source_rows, _ = _read_csv(source_path)
        return _gateway_unavailable_result(
            source_rows,
            "MatchGateway unavailable.",
        )

    source_rows, source_headers = _read_csv(source_path)
    target_rows, target_headers = _read_csv(target_path)

    if source_col not in source_headers:
        raise ValueError(f"Source column '{source_col}' not found in {source_path}.")
    if target_col not in target_headers:
        raise ValueError(f"Target column '{target_col}' not found in {target_path}.")

    try:
        import pandas as pd
    except ImportError as exc:
        raise ValueError("pandas is required for MatchGateway matching.") from exc

    source_df = pd.DataFrame(
        [dict(row, __source_id=idx) for idx, row in enumerate(source_rows)]
    )
    target_df = pd.DataFrame(
        [dict(row, __reference_id=idx) for idx, row in enumerate(target_rows)]
    )

    base_url = getattr(intel, "base_url", None) if intel else None
    try:
        gateway = await MatchGateway.create(base_url=base_url)
        if engine is not None:
            gateway._engine = engine
        if intel is not None:
            gateway._intel = intel

        # g-gremlin already has explicit columns; provide mappings so MatchGateway
        # can skip AI/heuristic config generation (faster + deterministic).
        explicit_mappings = [
            {"source": source_col, "reference": target_col, "weight": 1.0, "algorithm": "WRatio"}
        ]
        result = await gateway.match(
            source_df,
            target_df,
            source_col=source_col,
            target_col=target_col,
            threshold=threshold,
            mappings=explicit_mappings,
        )
    except Exception as exc:
        logger.warning("MatchGateway match failed: %s", exc)
        return GatewayUnavailableResult(source_df, str(exc))

    matches_df = result.matches if result else None
    best_matches = _best_match_by_row(matches_df)

    threshold_from_config = result.config_used.threshold if result else threshold

    return _write_engine_results(
        source_rows,
        source_headers,
        target_rows,
        target_headers,
        target_col,
        best_matches,
        output_path,
        threshold_from_config,
    )


def _gateway_unavailable_result(
    source_rows: List[Dict[str, str]],
    reason: str,
) -> "GatewayUnavailableResult":
    try:
        import pandas as pd
    except ImportError as exc:
        raise ValueError("pandas is required to build gateway-unavailable results.") from exc
    from fmatch.saas.services.match_gateway import GatewayUnavailableResult

    source_df = pd.DataFrame(source_rows)
    return GatewayUnavailableResult(source_df, reason)


def is_engine_available() -> bool:
    """Check if Engine API is available."""
    try:
        from fmatch.saas.services.engine_client import EngineClient  # noqa: F401

        return True
    except ImportError:
        return False


def _read_csv(path: Path) -> Tuple[List[Dict[str, str]], List[str]]:
    with path.open("r", newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError(f"CSV {path} is missing a header row.")
        rows = [dict(row) for row in reader]
        return rows, list(reader.fieldnames)


def _pick_key_column(headers: List[str], target_col: str) -> Optional[str]:
    for header in headers:
        lower = header.lower()
        if header == target_col:
            continue
        if "id" in lower or lower.endswith("_id"):
            return header
    return None


def _write_engine_results(
    source_rows: List[Dict[str, str]],
    source_headers: List[str],
    target_rows: List[Dict[str, str]],
    target_headers: List[str],
    target_col: str,
    best_matches: Dict[str, Dict[str, Any]],
    output_path: Path,
    threshold: float,
) -> FuzzyMatchSummary:
    summary = FuzzyMatchSummary(matched=0, low_confidence=0, no_match=0, total=len(source_rows))
    output_headers = list(source_headers)
    append_headers = ["matched_target_value", "matched_target_key", "match_score", "match_status"]
    for h in append_headers:
        if h not in output_headers:
            output_headers.append(h)

    target_key_col = _pick_key_column(target_headers, target_col)
    target_lookup: Dict[str, Dict[str, str]] = {str(idx): row for idx, row in enumerate(target_rows)}

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=output_headers)
        writer.writeheader()
        for idx, row in enumerate(source_rows):
            output_row = dict(row)
            match = best_matches.get(str(idx)) or {}
            target_id = (
                match.get("target_id")
                or match.get("reference_id")
                or match.get("__reference_id")
                or match.get("ref_id")
            )
            target_row = target_lookup.get(str(target_id)) if target_id is not None else None

            raw_score = match.get("score") if match else None
            if raw_score is None and match:
                raw_score = match.get("match_score")
            try:
                score = float(raw_score) if raw_score is not None else 0.0
            except (TypeError, ValueError):
                score = 0.0

            matched_value = target_row.get(target_col, "") if target_row else ""
            matched_key = target_row.get(target_key_col, "") if target_row and target_key_col else ""

            if target_row and score > 0.0:
                if score >= threshold:
                    status = "matched"
                    summary.matched += 1
                else:
                    status = "low_confidence"
                    summary.low_confidence += 1
            else:
                status = "no_match"
                summary.no_match += 1

            output_row["matched_target_value"] = matched_value
            output_row["matched_target_key"] = matched_key
            output_row["match_score"] = round(score, 3)
            output_row["match_status"] = status
            writer.writerow(output_row)

    return summary


async def _get_ai_match_config(
    source_df: Any,
    target_df: Any,
    intel: Any,
) -> Optional[Dict[str, Any]]:
    try:
        import httpx  # type: ignore
    except Exception:
        logger.info("httpx unavailable; skipping AI match-config.")
        return None

    base_url = getattr(intel, "base_url", "") or ""
    if not base_url:
        logger.info("Intelligence base_url unavailable; skipping AI match-config.")
        return None

    payload = {
        "source_schema": _df_to_schema(source_df),
        "reference_schema": _df_to_schema(target_df),
        "mode": "match",
    }
    headers = getattr(intel, "headers", None) or {}
    url = f"{base_url.rstrip('/')}/api/v2/ai/match-config"

    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(url, json=payload, headers=headers)
        if resp.status_code != 200:
            logger.info("AI match-config unavailable (status=%s).", resp.status_code)
            return None
        data = resp.json()
    except Exception as exc:
        logger.warning("AI match-config request failed: %s", exc)
        return None

    try:
        return _convert_ai_response(data, source_df, target_df)
    except Exception as exc:
        logger.warning("AI match-config parse failed: %s", exc)
        return None


def _df_to_schema(df: Any) -> Dict[str, Any]:
    columns: List[Dict[str, Any]] = []
    row_count = len(df) if df is not None else 0
    if df is None or not hasattr(df, "columns"):
        return {"columns": columns, "row_count": row_count}

    for col in df.columns:
        name = str(col)
        if name.startswith("__"):
            continue
        series = df[col]
        try:
            samples = series.dropna().head(5).tolist()
            fill_rate = float(series.notna().mean()) if row_count else 0.0
            data_type = str(series.dtype)
        except Exception:
            samples = []
            fill_rate = None
            data_type = None
        columns.append(
            {
                "name": name,
                "samples": [str(s) for s in samples],
                "data_type": data_type,
                "fill_rate": fill_rate,
            }
        )
    return {"columns": columns, "row_count": row_count}


def _convert_ai_response(
    ai_resp: Dict[str, Any],
    source_df: Any,
    target_df: Any,
) -> Dict[str, Any]:
    source_lookup = _column_lookup(source_df)
    reference_lookup = _column_lookup(target_df)

    mappings_out: List[Dict[str, Any]] = []
    for mapping in ai_resp.get("mappings", []) or []:
        if not isinstance(mapping, dict):
            continue
        raw_source = (mapping.get("source") or "").strip()
        raw_reference = (mapping.get("reference") or "").strip()
        if not raw_source:
            continue
        source_col = source_lookup.get(raw_source.lower())
        if not source_col:
            continue
        reference_col = reference_lookup.get(raw_reference.lower()) if raw_reference else None
        if not reference_col:
            continue

        match_type = (mapping.get("type") or "exact").lower()
        preferred_algo = "Exact" if match_type == "exact" else "TokenSetRatio"
        out = {
            "source": source_col,
            "reference": reference_col,
            "preferred_algo": preferred_algo,
        }
        weight = mapping.get("weight")
        if weight is not None:
            out["weight"] = weight
        mappings_out.append(out)

    option_hints = ai_resp.get("option_hints")
    if not isinstance(option_hints, dict):
        option_hints = {}

    threshold = option_hints.get("threshold", 0.8)
    try:
        threshold = float(threshold)
    except (TypeError, ValueError):
        threshold = 0.8
    if threshold > 1.0:
        threshold = threshold / 100.0
    threshold = max(0.0, min(1.0, threshold))

    return {
        "mappings": mappings_out,
        "field_mappings": mappings_out,
        "threshold": threshold,
        "option_hints": option_hints,
    }


def _column_lookup(df: Any) -> Dict[str, str]:
    if df is None or not hasattr(df, "columns"):
        return {}
    lookup: Dict[str, str] = {}
    for col in df.columns:
        name = str(col)
        if name.startswith("__"):
            continue
        lookup[name.lower()] = name
    return lookup


def _extract_blocking_strategy(
    config: Dict[str, Any],
    source_df: Any = None,
    target_df: Any = None,
    source_col: Optional[str] = None,
    target_col: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
    row_count = _max_row_count(source_df, target_df)
    require_blocking = row_count > LOCAL_MATCH_MAX_ROWS
    mappings = config.get("field_mappings") or config.get("mappings") or []

    blocking = config.get("blocking_strategy") or config.get("blocking") or {}
    if isinstance(blocking, dict):
        apply = blocking.get("apply_blocking")
        if apply is None:
            apply = blocking.get("enabled")
        src_col, ref_col, key_len = _parse_blocking_config(blocking)
        if src_col and ref_col and (apply is None or apply):
            logger.info(
                "Blocking: using explicit config (source=%s, ref=%s, len=%s)",
                src_col,
                ref_col,
                key_len,
            )
            return {
                "source_column": src_col,
                "reference_column": ref_col,
                "key_length": key_len,
            }

    hints = config.get("option_hints") or {}
    block_type = (hints.get("block") or "").lower()
    anchor = hints.get("anchor")

    src_cols = _list_df_columns(source_df)
    ref_cols = _list_df_columns(target_df)

    if block_type in {"domain", "state"}:
        src_block, ref_block = _match_blocking_pair(
            block_type, src_cols, ref_cols, mappings
        )
        if src_block and ref_block:
            if block_type == "domain":
                src_block, ref_block = _ensure_domain_block_columns(
                    source_df, target_df, src_block, ref_block
                )
            logger.info(
                "Blocking: %s on %s -> %s",
                block_type,
                src_block,
                ref_block,
            )
            return {
                "source_column": src_block,
                "reference_column": ref_block,
                "key_length": 3,
            }

    src_domain = _find_column_by_keywords(src_cols, DOMAIN_COLUMN_TOKENS)
    ref_domain = _find_column_by_keywords(ref_cols, DOMAIN_COLUMN_TOKENS)
    if src_domain and ref_domain:
        src_block, ref_block = _ensure_domain_block_columns(
            source_df, target_df, src_domain, ref_domain
        )
        logger.info("Blocking: domain on %s -> %s", src_block, ref_block)
        return {
            "source_column": src_block,
            "reference_column": ref_block,
            "key_length": 3,
        }

    src_email = _find_column_by_keywords(src_cols, EMAIL_COLUMN_TOKENS)
    ref_email = _find_column_by_keywords(ref_cols, EMAIL_COLUMN_TOKENS)
    if src_email and ref_email:
        src_block, ref_block = _ensure_email_domain_block_columns(
            source_df, target_df, src_email, ref_email
        )
        logger.info("Blocking: email-domain on %s -> %s", src_block, ref_block)
        return {
            "source_column": src_block,
            "reference_column": ref_block,
            "key_length": 3,
        }

    # CLI columns take priority - user explicitly specified what to match
    if source_col and target_col:
        logger.info("Blocking: name-prefix on CLI columns %s -> %s", source_col, target_col)
        return {
            "source_column": source_col,
            "reference_column": target_col,
            "key_length": 3,
        }

    # Fall back to AI anchor if no CLI columns
    if anchor:
        src_anchor = _match_column_name(anchor, src_cols)
        ref_anchor = _match_column_name(anchor, ref_cols)
        if not src_anchor and ref_anchor:
            src_anchor = _find_source_for_reference(ref_anchor, mappings) or _match_column_name(
                ref_anchor, src_cols
            )
        if not ref_anchor and src_anchor:
            ref_anchor = _find_reference_for_source(src_anchor, mappings) or _match_column_name(
                src_anchor, ref_cols
            )
        if src_anchor and ref_anchor:
            logger.info(
                "Blocking: name-prefix on anchor=%s -> %s (AI block=%s)",
                src_anchor,
                ref_anchor,
                block_type or "none",
            )
            return {
                "source_column": src_anchor,
                "reference_column": ref_anchor,
                "key_length": 3,
            }

    src_company = _find_column_by_keywords(src_cols, COMPANY_COLUMN_TOKENS)
    ref_company = _find_column_by_keywords(ref_cols, COMPANY_COLUMN_TOKENS)
    if src_company and ref_company:
        logger.info("Blocking: name-prefix on %s -> %s", src_company, ref_company)
        return {
            "source_column": src_company,
            "reference_column": ref_company,
            "key_length": 3,
        }

    src_map, ref_map = _select_mapping_for_blocking(mappings)
    if src_map and ref_map:
        logger.info("Blocking: name-prefix on mapping %s -> %s", src_map, ref_map)
        return {
            "source_column": src_map,
            "reference_column": ref_map,
            "key_length": 3,
        }

    if require_blocking and src_cols and ref_cols:
        src_fallback = src_cols[0]
        ref_fallback = _match_column_name(src_fallback, ref_cols) or ref_cols[0]
        logger.warning(
            "Blocking: forced fallback on %s -> %s for large dataset",
            src_fallback,
            ref_fallback,
        )
        return {
            "source_column": src_fallback,
            "reference_column": ref_fallback,
            "key_length": 3,
        }

    logger.warning("Blocking: could not determine blocking columns")
    return None


DOMAIN_COLUMN_TOKENS = (
    "domain",
    "website",
    "web",
    "url",
)
EMAIL_COLUMN_TOKENS = (
    "email",
    "e-mail",
    "email_address",
    "emailaddress",
    "work_email",
    "business_email",
    "primary_email",
    "contact_email",
)
COMPANY_COLUMN_TOKENS = (
    "company",
    "company_name",
    "account",
    "account_name",
    "organization",
    "org",
    "org_name",
    "employer",
    "business",
    "business_name",
    "name",
)
STATE_COLUMN_TOKENS = ("state", "region", "province")


def _parse_blocking_config(
    blocking: Dict[str, Any]
) -> Tuple[Optional[str], Optional[str], int]:
    source_col = blocking.get("source_block_col") or blocking.get("source_column")
    ref_col = blocking.get("ref_block_col") or blocking.get("reference_column")
    if not source_col and not ref_col:
        column = blocking.get("column")
        if column:
            source_col = column
            ref_col = column
    key_len = (
        blocking.get("block_key_len")
        or blocking.get("key_length")
        or blocking.get("key_len")
        or 3
    )
    keys = blocking.get("keys") or blocking.get("blocking_keys")
    if keys and isinstance(keys, list) and len(keys) > 0:
        first = keys[0]
        if isinstance(first, dict):
            source_col = source_col or first.get("source_column") or first.get("column")
            ref_col = ref_col or first.get("reference_column") or first.get("column")
            key_len = (
                first.get("key_length")
                or first.get("key_len")
                or first.get("block_key_len")
                or key_len
            )
        elif isinstance(first, str):
            source_col = source_col or first
            ref_col = ref_col or first

    if source_col and not ref_col:
        ref_col = source_col
    if ref_col and not source_col:
        source_col = ref_col

    try:
        key_len = int(key_len)
    except (TypeError, ValueError):
        key_len = 3

    return source_col, ref_col, key_len


def _max_row_count(source_df: Any, target_df: Any) -> int:
    counts = []
    for df in (source_df, target_df):
        if df is None:
            continue
        try:
            counts.append(len(df))
        except Exception:
            continue
    return max(counts) if counts else 0


def _list_df_columns(df: Any) -> List[str]:
    if df is None or not hasattr(df, "columns"):
        return []
    columns = []
    for col in df.columns:
        name = str(col)
        if name.startswith("__"):
            continue
        columns.append(name)
    return columns


def _find_column_by_keywords(columns: List[str], keywords: Tuple[str, ...]) -> Optional[str]:
    for col in columns:
        lowered = col.lower()
        if any(token in lowered for token in keywords):
            return col
    return None


def _match_column_name(name: Optional[str], columns: List[str]) -> Optional[str]:
    if not name:
        return None
    lower = name.strip().lower()
    if not lower:
        return None
    for col in columns:
        if col.lower() == lower:
            return col
    for col in columns:
        if lower in col.lower():
            return col
    return None


def _find_reference_for_source(
    source: str, mappings: List[Dict[str, Any]]
) -> Optional[str]:
    source_lower = source.lower()
    for mapping in mappings:
        if str(mapping.get("source", "")).lower() == source_lower:
            return mapping.get("reference") or mapping.get("ref")
    return None


def _find_source_for_reference(
    reference: str, mappings: List[Dict[str, Any]]
) -> Optional[str]:
    ref_lower = reference.lower()
    for mapping in mappings:
        ref_value = mapping.get("reference") or mapping.get("ref")
        if str(ref_value or "").lower() == ref_lower:
            return mapping.get("source")
    return None


def _select_mapping_for_blocking(
    mappings: List[Dict[str, Any]]
) -> Tuple[Optional[str], Optional[str]]:
    for tokens in (DOMAIN_COLUMN_TOKENS, EMAIL_COLUMN_TOKENS, COMPANY_COLUMN_TOKENS):
        for mapping in mappings:
            src = mapping.get("source")
            ref = mapping.get("reference") or mapping.get("ref")
            if not src or not ref:
                continue
            if _column_matches_tokens(str(src), tokens) or _column_matches_tokens(str(ref), tokens):
                return str(src), str(ref)
    for mapping in mappings:
        src = mapping.get("source")
        ref = mapping.get("reference") or mapping.get("ref")
        if src and ref:
            return str(src), str(ref)
    return None, None


def _column_matches_tokens(name: str, tokens: Tuple[str, ...]) -> bool:
    lowered = name.lower()
    return any(token in lowered for token in tokens)


def _match_blocking_pair(
    block_type: str,
    src_cols: List[str],
    ref_cols: List[str],
    mappings: List[Dict[str, Any]],
) -> Tuple[Optional[str], Optional[str]]:
    if block_type == "domain":
        tokens = DOMAIN_COLUMN_TOKENS
    elif block_type == "state":
        tokens = STATE_COLUMN_TOKENS
    else:
        tokens = COMPANY_COLUMN_TOKENS

    for mapping in mappings:
        src = mapping.get("source")
        ref = mapping.get("reference") or mapping.get("ref")
        if not src or not ref:
            continue
        if _column_matches_tokens(str(src), tokens) or _column_matches_tokens(
            str(ref), tokens
        ):
            return str(src), str(ref)

    src_match = _find_column_by_keywords(src_cols, tokens)
    ref_match = _find_column_by_keywords(ref_cols, tokens)
    if src_match and not ref_match:
        ref_match = _match_column_name(src_match, ref_cols)
    if ref_match and not src_match:
        src_match = _match_column_name(ref_match, src_cols)
    return src_match, ref_match


def _ensure_domain_block_columns(
    source_df: Any,
    target_df: Any,
    source_col: str,
    target_col: str,
) -> Tuple[str, str]:
    if source_df is None or target_df is None:
        return source_col, target_col
    block_col = _unique_block_column(source_df, target_df, "__block_domain")
    try:
        source_df[block_col] = source_df[source_col].map(_normalize_domain_value)
        target_df[block_col] = target_df[target_col].map(_normalize_domain_value)
        return block_col, block_col
    except Exception:
        return source_col, target_col


def _ensure_email_domain_block_columns(
    source_df: Any,
    target_df: Any,
    source_col: str,
    target_col: str,
) -> Tuple[str, str]:
    if source_df is None or target_df is None:
        return source_col, target_col
    block_col = _unique_block_column(source_df, target_df, "__block_email_domain")
    try:
        source_df[block_col] = source_df[source_col].map(_normalize_domain_value)
        target_df[block_col] = target_df[target_col].map(_normalize_domain_value)
        return block_col, block_col
    except Exception:
        return source_col, target_col


def _normalize_domain_value(value: Any) -> str:
    text = str(value or "").strip().lower()
    if not text:
        return ""
    if "@" in text:
        text = text.split("@", 1)[1]
    if "://" in text:
        text = text.split("://", 1)[1]
    text = text.split("/", 1)[0]
    if text.startswith("www."):
        text = text[4:]
    return text


def _unique_block_column(source_df: Any, target_df: Any, base: str) -> str:
    existing = set()
    for df in (source_df, target_df):
        if df is None or not hasattr(df, "columns"):
            continue
        existing.update(str(col) for col in df.columns)
    if base not in existing:
        return base
    idx = 1
    while f"{base}_{idx}" in existing:
        idx += 1
    return f"{base}_{idx}"


def _best_match_by_row(matches: Any) -> Dict[str, Dict[str, Any]]:
    if matches is None:
        return {}
    df = matches.copy()
    # Normalize score column - Engine returns confidence_score (preferred over 'score' which may be 0)
    if "confidence_score" in df.columns:
        df["score"] = df["confidence_score"]
    elif "match_score" in df.columns:
        df["score"] = df["match_score"]
    # Normalize to 0-1 scale if needed
    if "score" in df.columns and df["score"].max() > 1.0:
        df["score"] = df["score"] / 100.0
    # Normalize source_id column - Engine returns __row_id
    if "source_id" not in df.columns:
        if "__source_id" in df.columns:
            df["source_id"] = df["__source_id"]
        elif "__row_id" in df.columns:
            df["source_id"] = df["__row_id"]
        elif "src_id" in df.columns:
            df["source_id"] = df["src_id"]
    # Normalize target_id column - Engine returns __target_id
    if "target_id" not in df.columns:
        if "reference_id" in df.columns:
            df["target_id"] = df["reference_id"]
        elif "__reference_id" in df.columns:
            df["target_id"] = df["__reference_id"]
        elif "__target_id" in df.columns:
            df["target_id"] = df["__target_id"]
        elif "ref_id" in df.columns:
            df["target_id"] = df["ref_id"]
    df = df.sort_values(by=["score"], ascending=False, na_position="last")
    winners: Dict[str, Dict[str, Any]] = {}
    for _, row in df.iterrows():
        src_id = row.get("source_id")
        if src_id is None:
            continue
        key = str(src_id)
        if key in winners:
            continue
        winners[key] = row.to_dict()
    return winners
