"""Template binding logic for canonical role mapping."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Optional

from g_gremlin.tableau.column_analyzer import ColumnProfile, analyze_csv_columns
from g_gremlin.tableau.template_models import CanonicalRoleSpec, TemplateManifest


@dataclass
class FieldMapping:
    canonical_role: str
    csv_column: str
    csv_type: str
    cardinality: int
    nullable: bool
    confidence: int
    explanation: str

    def to_dict(self) -> Dict[str, object]:
        return {
            "canonical_role": self.canonical_role,
            "csv_column": self.csv_column,
            "csv_type": self.csv_type,
            "cardinality": self.cardinality,
            "nullable": self.nullable,
            "confidence": self.confidence,
            "explanation": self.explanation,
        }


@dataclass
class BindingError:
    error_type: str
    field_role: str
    message: str
    suggestion: Optional[str] = None

    def to_dict(self) -> Dict[str, object]:
        return {
            "error_type": self.error_type,
            "field_role": self.field_role,
            "message": self.message,
            "suggestion": self.suggestion,
        }


@dataclass
class BindingResult:
    success: bool
    mappings: List[FieldMapping]
    errors: List[BindingError]
    warnings: List[str] = field(default_factory=list)
    mapping_explanations: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, object]:
        return {
            "success": self.success,
            "mappings": [m.to_dict() for m in self.mappings],
            "errors": [e.to_dict() for e in self.errors],
            "warnings": self.warnings,
            "mapping_explanations": self.mapping_explanations,
        }


def bind_template(
    template_manifest: TemplateManifest,
    csv_path,
    explicit_mappings: Optional[Dict[str, str]] = None,
) -> BindingResult:
    """Bind CSV columns to canonical template roles."""
    column_profiles = analyze_csv_columns(csv_path)
    profile_map = {p.name: p for p in column_profiles}

    mappings: List[FieldMapping] = []
    explanations: List[str] = []
    pre_errors: List[BindingError] = []

    explicit_mappings = explicit_mappings or {}
    for canonical_role, csv_col in explicit_mappings.items():
        profile = profile_map.get(csv_col)
        if not profile:
            pre_errors.append(
                BindingError(
                    error_type="missing_column",
                    field_role=canonical_role,
                    message=f"Column '{csv_col}' not found for role '{canonical_role}'",
                    suggestion=f"Use --map {canonical_role}=<existing_column>",
                )
            )
            explanations.append(f"Explicit mapping failed: '{csv_col}' not found")
            continue
        mappings.append(
            FieldMapping(
                canonical_role=canonical_role,
                csv_column=csv_col,
                csv_type=profile.inferred_type,
                cardinality=profile.cardinality,
                nullable=profile.nullable,
                confidence=100,
                explanation=f"Explicitly mapped '{csv_col}' to {canonical_role}",
            )
        )
        explanations.append(f"Explicitly mapped '{csv_col}' -> {canonical_role}")

    mapped_roles = {m.canonical_role for m in mappings}
    mapped_columns = {m.csv_column for m in mappings}

    for role_spec in template_manifest.canonical_roles:
        if role_spec.id in mapped_roles:
            continue
        if not role_spec.required:
            continue

        candidates = find_candidates_for_role(role_spec, column_profiles, mapped_columns)
        if candidates:
            best = select_best_candidate(candidates)
            mappings.append(best)
            mapped_roles.add(best.canonical_role)
            mapped_columns.add(best.csv_column)
            explanations.append(best.explanation)

    errors = pre_errors + validate_mappings(mappings, template_manifest, column_profiles)

    return BindingResult(
        success=len(errors) == 0,
        mappings=mappings,
        errors=errors,
        warnings=[],
        mapping_explanations=explanations,
    )


def find_candidates_for_role(
    role_spec: CanonicalRoleSpec,
    profiles: List[ColumnProfile],
    exclude_columns: Optional[set[str]] = None,
) -> List[FieldMapping]:
    exclude_columns = exclude_columns or set()
    candidates: List[FieldMapping] = []
    for profile in profiles:
        if profile.name in exclude_columns:
            continue
        if not _semantic_match(role_spec.semantic_types, profile.semantic_types):
            continue
        confidence = _score_candidate(role_spec, profile)
        explanation = (
            f"Auto-mapped '{profile.name}' -> {role_spec.id} "
            f"({profile.inferred_type}, {profile.cardinality} uniques, confidence {confidence}%)"
        )
        candidates.append(
            FieldMapping(
                canonical_role=role_spec.id,
                csv_column=profile.name,
                csv_type=profile.inferred_type,
                cardinality=profile.cardinality,
                nullable=profile.nullable,
                confidence=confidence,
                explanation=explanation,
            )
        )
    return candidates


def select_best_candidate(candidates: List[FieldMapping]) -> FieldMapping:
    return sorted(candidates, key=lambda c: c.confidence, reverse=True)[0]


def validate_mappings(
    mappings: List[FieldMapping],
    manifest: TemplateManifest,
    profiles: List[ColumnProfile],
) -> List[BindingError]:
    errors: List[BindingError] = []
    mapped_roles = {m.canonical_role for m in mappings}
    profile_map = {p.name: p for p in profiles}

    for role_spec in manifest.canonical_roles:
        if role_spec.required and role_spec.id not in mapped_roles:
            errors.append(
                BindingError(
                    error_type="missing_required_field",
                    field_role=role_spec.id,
                    message=f"Required role '{role_spec.display_name}' not mapped",
                    suggestion=f"Use --map {role_spec.id}=YourColumnName",
                )
            )

    for mapping in mappings:
        role_spec = next((r for r in manifest.canonical_roles if r.id == mapping.canonical_role), None)
        profile = profile_map.get(mapping.csv_column)
        if not role_spec or not profile:
            if not profile:
                errors.append(
                    BindingError(
                        error_type="missing_column",
                        field_role=mapping.canonical_role,
                        message=f"Column '{mapping.csv_column}' not found",
                        suggestion=f"Use --map {mapping.canonical_role}=<existing_column>",
                    )
                )
            continue
        if not _semantic_match(role_spec.semantic_types, profile.semantic_types):
            errors.append(
                BindingError(
                    error_type="type_mismatch",
                    field_role=mapping.canonical_role,
                    message=(
                        f"Column '{mapping.csv_column}' has type '{profile.inferred_type}', "
                        f"expected {role_spec.semantic_types}"
                    ),
                    suggestion="Convert column to expected type or choose different field",
                )
            )
        if role_spec.cardinality:
            max_card = role_spec.cardinality.max
            min_card = role_spec.cardinality.min
            if max_card is not None and profile.cardinality > max_card:
                errors.append(
                    BindingError(
                        error_type="cardinality_violation",
                        field_role=mapping.canonical_role,
                        message=(
                            f"Column '{mapping.csv_column}' has {profile.cardinality} unique values, "
                            f"max allowed is {max_card}"
                        ),
                        suggestion="Filter data or use table template for high-cardinality data",
                    )
                )
            if min_card is not None and profile.cardinality < min_card:
                errors.append(
                    BindingError(
                        error_type="cardinality_violation",
                        field_role=mapping.canonical_role,
                        message=(
                            f"Column '{mapping.csv_column}' has {profile.cardinality} unique values, "
                            f"min required is {min_card}"
                        ),
                        suggestion="Ensure data has enough distinct values",
                    )
                )

    return errors


def _semantic_match(role_types: List[str], column_types: List[str]) -> bool:
    role_set = {t.lower() for t in role_types}
    col_set = {t.lower() for t in column_types}
    if "numeric" in role_set and ("numeric" in col_set or "integer" in col_set or "float" in col_set):
        return True
    if "integer" in role_set and "integer" in col_set:
        return True
    if "float" in role_set and "float" in col_set:
        return True
    if "date" in role_set and "date" in col_set:
        return True
    if "string" in role_set and "string" in col_set:
        return True
    return bool(role_set & col_set)


def _score_candidate(role_spec: CanonicalRoleSpec, profile: ColumnProfile) -> int:
    score = 60
    if _semantic_match(role_spec.semantic_types, profile.semantic_types):
        score += 20
    if role_spec.cardinality:
        if role_spec.cardinality.recommended_max and profile.cardinality <= role_spec.cardinality.recommended_max:
            score += 10
        if role_spec.cardinality.max and profile.cardinality <= role_spec.cardinality.max:
            score += 5
    if not profile.nullable:
        score += 5
    return min(score, 100)


__all__ = [
    "FieldMapping",
    "BindingError",
    "BindingResult",
    "bind_template",
    "validate_mappings",
]
