"""Top-level reconcile() function for local identity resolution."""
from __future__ import annotations

import json
import warnings as _warnings
from dataclasses import dataclass, field
from typing import Any

from .source import Source
from .spec import Spec
from .validate import validate


@dataclass
class ReconcileResult:
    """Result of a local reconciliation run."""

    clusters: list[list[str]]
    golden_records: list[dict[str, str]]
    decisions: list[dict[str, Any]]
    telemetry: dict[str, Any]

    @property
    def cluster_count(self) -> int:
        return len(self.clusters)

    @property
    def merge_rate(self) -> float:
        """Fraction of input entities that were merged (1 - clusters/entities)."""
        total = sum(len(c) for c in self.clusters)
        if total == 0:
            return 0.0
        return 1.0 - (len(self.clusters) / total)

    def to_pandas(self) -> Any:
        """Convert golden records to a pandas DataFrame (requires pandas)."""
        try:
            import pandas as pd
        except ImportError:
            raise ImportError(
                "ReconcileResult.to_pandas() requires pandas: pip install pandas"
            ) from None
        return pd.DataFrame(self.golden_records)


def _validate_source_attributes(
    sources: list[Source],
    spec_sources: list[dict[str, Any]],
) -> tuple[list[str], list[str]]:
    """Check source columns against spec attribute mappings.

    Returns (errors, warnings).
    """
    errors: list[str] = []
    warnings: list[str] = []

    spec_by_name = {s.get("name", ""): s for s in spec_sources}

    for source in sources:
        if source.name not in spec_by_name:
            errors.append(
                f"Source '{source.name}' not in spec. "
                f"Declared sources: {sorted(spec_by_name)}"
            )
            continue

        attrs = spec_by_name[source.name].get("attributes", {})
        if not attrs:
            continue

        actual = {col.name for col in source.schema().columns}
        for canonical, source_col in attrs.items():
            if source_col not in actual:
                lower_map = {c.lower(): c for c in actual}
                suggestion = lower_map.get(source_col.lower())
                msg = (
                    f"Source '{source.name}': column '{source_col}' "
                    f"(mapped to '{canonical}') not found."
                )
                if suggestion:
                    msg += f" Did you mean '{suggestion}'?"
                else:
                    msg += f" Available: {sorted(actual)}"
                errors.append(msg)

    for name in spec_by_name:
        if not any(s.name == name for s in sources):
            warnings.append(f"Spec declares source '{name}' but none provided")

    return errors, warnings


def reconcile(
    sources: list[Source],
    spec: Spec,
    *,
    return_entity_map: bool = False,
) -> ReconcileResult:
    """Run local identity resolution across sources using the given spec.

    1. Validate the spec
    2. Collect entities from all sources
    3. Run the Rust reconciliation engine via PyO3
    4. Return structured results

    Args:
        sources: List of data sources to reconcile.
        spec: The identity spec defining rules and thresholds.
        return_entity_map: If True, attach ``_entity_map`` to the result
            mapping internal UUIDs to ``(source_name, external_id)`` tuples.
            Required for ``evaluate()``.
    """
    from kanoniv._native import reconcile_local

    # 1. Validate spec
    result = validate(spec)
    result.raise_on_error()

    # 2. Pre-flight: check source columns match spec attribute mappings
    src_errors, src_warnings = _validate_source_attributes(sources, spec.sources)
    if src_errors:
        raise ValueError(
            "Source-spec mismatch:\n" + "\n".join(f"  - {e}" for e in src_errors)
        )
    for w in src_warnings:
        _warnings.warn(w, UserWarning, stacklevel=2)

    # 3. Extract entity_type from spec
    entity = spec.entity
    if isinstance(entity, dict):
        entity_type = entity.get("name", "entity")
    else:
        entity_type = entity or "entity"

    # 4. Build attribute mappings from spec (canonical_name → source_column per source)
    #    spec.sources[i].attributes = {"email": "email_address", "phone": "phone_number"}
    #    means: entity.data["email"] = row["email_address"]
    attr_maps: dict[str, dict[str, str]] = {}
    for spec_src in spec.sources:
        src_name = spec_src.get("name", "")
        attrs = spec_src.get("attributes", {})
        if attrs and src_name:
            # attrs is {canonical: source_column}, we need {source_column: canonical}
            attr_maps[src_name] = {v: k for k, v in attrs.items()}

    # 5. Collect entities from all sources, applying attribute mappings
    all_entities: list[dict[str, Any]] = []
    for source in sources:
        entities = source.to_entities(entity_type)
        reverse_map = attr_maps.get(source.name, {})
        if reverse_map:
            for entity in entities:
                raw_data = entity["data"]
                mapped: dict[str, str] = {}
                for col, val in raw_data.items():
                    canonical = reverse_map.get(col, col)
                    mapped[canonical] = val
                entity["data"] = mapped
        all_entities.extend(entities)

    # 6. Build entity map if requested (UUID -> (source_name, external_id))
    entity_map: dict[str, tuple[str, str]] | None = None
    if return_entity_map:
        entity_map = {
            e["id"]: (e["source_name"], e["external_id"])
            for e in all_entities
        }

    # 7. Serialize and call native engine
    entities_json = json.dumps(all_entities)
    raw = reconcile_local(spec.raw, entities_json)

    # 8. Unpack into ReconcileResult
    result = ReconcileResult(
        clusters=raw.get("clusters", []),
        golden_records=raw.get("golden_records", []),
        decisions=raw.get("decisions", []),
        telemetry=raw.get("telemetry", {}),
    )
    if entity_map is not None:
        result._entity_map = entity_map  # type: ignore[attr-defined]
    return result
