"""BLCE AI Agents — intelligent enrichment for business logic comprehension.

Phase 9 agents provide AI-assisted analysis when deterministic parsers
produce low-confidence results.  Each agent follows a common pattern:

    agent = SomeAgent(config)
    result = agent.analyze(artifact)

All agents work with heuristic fallbacks when Cortex/LLM services are
unavailable, ensuring the pipeline never blocks on external dependencies.
"""
from __future__ import annotations

import logging
import re
import time
from typing import Any, Dict, List, Optional

from .constants import MeasureAggregation, ToolClassification
from .contracts import (
    AgentTrace,
    CrossReferenceMapping,
    LogicArtifact,
    QualityPolicy,
    SemanticAnalysis,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Agent registry
# ---------------------------------------------------------------------------

_AGENT_REGISTRY: Dict[str, type] = {}


def register_agent(cls):
    """Decorator to register an agent class."""
    _AGENT_REGISTRY[cls.__name__] = cls
    return cls


def list_agents() -> List[Dict[str, str]]:
    """List all registered BLCE agents."""
    return [
        {
            "name": name,
            "description": (cls.__doc__ or "").strip().split("\n")[0],
            "priority": getattr(cls, "priority", "P2"),
        }
        for name, cls in _AGENT_REGISTRY.items()
    ]


# ---------------------------------------------------------------------------
# Base agent
# ---------------------------------------------------------------------------

class BLCEAgent:
    """Base class for BLCE AI agents.

    Provides common infrastructure: tracing, timing, error handling.
    Subclasses implement `_run_analysis()`.
    """

    agent_name: str = "BLCEAgent"
    priority: str = "P2"

    def __init__(self, **kwargs):
        self._traces: List[AgentTrace] = []

    def _create_trace(self, action: str, artifact_ids: List[str]) -> AgentTrace:
        return AgentTrace(
            agent_name=self.agent_name,
            artifact_ids=artifact_ids,
            action=action,
        )

    def _finish_trace(self, trace: AgentTrace, output_summary: str,
                      duration: float, success: bool = True,
                      error: Optional[str] = None):
        trace.output_summary = output_summary
        trace.duration_seconds = round(duration, 3)
        trace.success = success
        trace.error = error
        self._traces.append(trace)

    @property
    def traces(self) -> List[Dict[str, Any]]:
        return [t.model_dump() for t in self._traces]

    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Unified entry point for the AgentRuntime.

        Subclasses override to read from *context* and return results.
        """
        raise NotImplementedError(
            f"{self.agent_name}.execute() not implemented"
        )


# ---------------------------------------------------------------------------
# P1: ReportSemanticAgent
# ---------------------------------------------------------------------------

@register_agent
class ReportSemanticAgent(BLCEAgent):
    """Analyzes business intent of logic artifacts using semantic heuristics.

    When deterministic parser confidence is below threshold, this agent
    applies domain-aware heuristics to infer business intent, explain
    measures in plain English, and boost confidence scores.
    """

    agent_name = "ReportSemanticAgent"
    priority = "P1"

    # Domain keyword → tag mapping
    _DOMAIN_KEYWORDS = {
        "revenue": ["revenue", "income", "sales", "billing", "receivable"],
        "production": ["production", "volume", "boe", "mcf", "barrel", "well"],
        "expense": ["expense", "cost", "opex", "capex", "overhead", "payroll"],
        "inventory": ["inventory", "stock", "warehouse", "sku"],
        "accounting": ["gl", "journal", "ledger", "debit", "credit", "account"],
        "lease": ["lease", "royalty", "interest", "division_order"],
        "regulatory": ["regulatory", "compliance", "permit", "filing"],
    }

    # Aggregation → plain-English pattern
    _AGG_DESCRIPTIONS = {
        MeasureAggregation.SUM: "total {name}",
        MeasureAggregation.COUNT: "count of {name}",
        MeasureAggregation.COUNT_DISTINCT: "distinct count of {name}",
        MeasureAggregation.AVG: "average {name}",
        MeasureAggregation.MIN: "minimum {name}",
        MeasureAggregation.MAX: "maximum {name}",
    }

    def __init__(self, confidence_threshold: float = 0.60, **kwargs):
        super().__init__(**kwargs)
        self.confidence_threshold = confidence_threshold

    def analyze(self, artifact: LogicArtifact) -> SemanticAnalysis:
        """Analyze a single artifact for business intent."""
        t0 = time.time()
        trace = self._create_trace("semantic_analysis", [artifact.artifact_id])

        # Detect domain tags
        domain_tags = self._detect_domains(artifact)

        # Generate measure explanations
        measure_explanations = self._explain_measures(artifact)

        # Build business intent description
        business_intent = self._describe_intent(artifact, domain_tags)

        # Calculate boosted confidence
        boosted = self._boost_confidence(artifact, domain_tags,
                                          measure_explanations)

        # Warnings
        warnings = self._detect_warnings(artifact)

        result = SemanticAnalysis(
            artifact_id=artifact.artifact_id,
            original_confidence=artifact.confidence,
            boosted_confidence=boosted,
            business_intent=business_intent,
            domain_tags=domain_tags,
            measure_explanations=measure_explanations,
            warnings=warnings,
        )

        duration = time.time() - t0
        self._finish_trace(
            trace,
            f"Analyzed artifact, confidence {artifact.confidence:.2f} -> {boosted:.2f}",
            duration,
        )
        return result

    def _detect_domains(self, artifact: LogicArtifact) -> List[str]:
        """Detect business domains from artifact content."""
        text_pool = []
        for m in artifact.objects.measures:
            text_pool.append((m.name or "").lower())
            text_pool.append((m.alias or "").lower())
            text_pool.extend(c.lower() for c in m.source_columns)
        for d in artifact.objects.dependencies:
            text_pool.append(d.name.lower())
        for g in artifact.objects.grain_columns:
            text_pool.append(g.column.lower())
        text_pool.append(artifact.source_name.lower())

        all_text = " ".join(text_pool)
        found = []
        for domain, keywords in self._DOMAIN_KEYWORDS.items():
            if any(kw in all_text for kw in keywords):
                found.append(domain)
        return sorted(set(found)) or ["general"]

    def _explain_measures(self, artifact: LogicArtifact) -> Dict[str, str]:
        """Generate plain-English explanations for each measure."""
        explanations = {}
        for m in artifact.objects.measures:
            name = m.alias or m.name or "unnamed"
            # Use aggregation pattern
            template = self._AGG_DESCRIPTIONS.get(
                m.aggregation,
                "{name} (custom calculation)",
            )
            desc = template.format(name=self._humanize(name))

            # Add source column context
            if m.source_columns:
                desc += f" from {', '.join(m.source_columns)}"

            explanations[name] = desc
        return explanations

    def _describe_intent(self, artifact: LogicArtifact,
                         domains: List[str]) -> str:
        """Generate a plain-English description of the artifact's purpose."""
        parts = []

        # Source context
        if artifact.source_name:
            parts.append(f"This logic from '{artifact.source_name}'")
        else:
            parts.append(f"This {artifact.source_type.value} logic")

        # Measure summary
        measures = artifact.objects.measures
        if measures:
            names = [m.alias or m.name for m in measures[:3] if m.name or m.alias]
            if names:
                parts.append(f"calculates {', '.join(names)}")
                if len(measures) > 3:
                    parts.append(f"and {len(measures) - 3} more measures")

        # Grain context
        grains = artifact.objects.grain_columns
        if grains:
            cols = [g.column for g in grains]
            parts.append(f"at the {' × '.join(cols)} grain")

        # Domain context
        if domains and domains != ["general"]:
            parts.append(f"in the {', '.join(domains)} domain(s)")

        return " ".join(parts) + "."

    def _boost_confidence(self, artifact: LogicArtifact,
                          domains: List[str],
                          explanations: Dict[str, str]) -> float:
        """Calculate a boosted confidence score."""
        base = artifact.confidence
        boost = 0.0

        # Boost for having identifiable domains
        if domains and domains != ["general"]:
            boost += 0.05

        # Boost for each explained measure
        if explanations:
            boost += min(0.10, 0.02 * len(explanations))

        # Boost for having grain
        if artifact.objects.grain_columns:
            boost += 0.05

        # Boost for having dependencies
        if artifact.objects.dependencies:
            boost += 0.05

        return min(1.0, round(base + boost, 2))

    def _detect_warnings(self, artifact: LogicArtifact) -> List[str]:
        """Detect potential issues in the artifact."""
        warnings = []
        if not artifact.objects.measures:
            warnings.append("No measures detected — may be a DDL or DML statement")
        if not artifact.objects.grain_columns:
            warnings.append("No grain columns — aggregation level unclear")
        if artifact.confidence < 0.30:
            warnings.append("Very low confidence — manual review recommended")
        # Check for CUSTOM aggregations
        custom_count = sum(
            1 for m in artifact.objects.measures
            if m.aggregation == MeasureAggregation.CUSTOM
        )
        if custom_count > 0:
            warnings.append(
                f"{custom_count} measure(s) use CUSTOM aggregation — "
                "expressions may need manual verification"
            )
        return warnings

    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Runtime entry point — analyze artifacts below confidence threshold."""
        arts = [
            LogicArtifact(**a) for a in context.get("artifacts", [])
            if a.get("confidence", 1.0) < self.confidence_threshold
        ]
        results = [self.analyze(art).model_dump() for art in arts]
        return {"semantic_analyses": results}

    @staticmethod
    def _humanize(name: str) -> str:
        """Convert snake_case to human-readable."""
        return re.sub(r"[_\-]+", " ", name).strip()


# ---------------------------------------------------------------------------
# P1: CrossReferenceMasterDataAgent
# ---------------------------------------------------------------------------

@register_agent
class CrossReferenceMasterDataAgent(BLCEAgent):
    """Discovers cross-system entity mappings using column-name heuristics.

    Goes beyond the deterministic crossref module by applying fuzzy
    matching and domain-aware entity resolution to find mappings that
    exact string matching would miss.
    """

    agent_name = "CrossReferenceMasterDataAgent"
    priority = "P1"

    # Common entity synonym groups
    _ENTITY_SYNONYMS = {
        "well": ["well_id", "well_no", "well_num", "wellid", "well_number",
                 "api_no", "api_number", "api"],
        "account": ["account_id", "acct_id", "account_no", "acct_no",
                    "gl_account", "account_number"],
        "vendor": ["vendor_id", "vendor_no", "supplier_id", "payee_id"],
        "owner": ["owner_id", "owner_no", "owner_number", "interest_owner"],
        "lease": ["lease_id", "lease_no", "lease_number", "lease_name"],
        "date": ["date", "period", "month", "year", "fiscal_period",
                 "report_date", "effective_date", "transaction_date"],
        "product": ["product_id", "product_code", "product_type",
                    "commodity", "commodity_type"],
    }

    def analyze(
        self,
        artifacts: List[LogicArtifact],
        min_confidence: float = 0.50,
    ) -> List[CrossReferenceMapping]:
        """Find cross-system entity mappings across artifacts."""
        t0 = time.time()
        trace = self._create_trace(
            "cross_reference",
            [a.artifact_id for a in artifacts],
        )

        if len(artifacts) < 2:
            self._finish_trace(trace, "Need 2+ artifacts", time.time() - t0)
            return []

        mappings = []

        # Build column inventory per artifact
        inventories = []
        for art in artifacts:
            cols = set()
            for g in art.objects.grain_columns:
                if g.column:
                    cols.add(g.column.lower())
            for m in art.objects.measures:
                for sc in m.source_columns:
                    cols.add(sc.lower())
            for d in art.objects.dependencies:
                if d.name:
                    cols.add(d.name.lower())
            inventories.append({
                "artifact_id": art.artifact_id,
                "source": art.source_name or art.artifact_id,
                "columns": cols,
            })

        # Cross-match using synonym groups
        for entity_type, synonyms in self._ENTITY_SYNONYMS.items():
            syn_set = set(s.lower() for s in synonyms)

            # Find which artifacts have columns matching this synonym group
            matches = []
            for inv in inventories:
                matched_cols = inv["columns"] & syn_set
                if matched_cols:
                    matches.append({
                        "artifact_id": inv["artifact_id"],
                        "source": inv["source"],
                        "matched_columns": sorted(matched_cols),
                    })

            # If 2+ artifacts match the same entity type, create mappings
            if len(matches) >= 2:
                for i in range(len(matches)):
                    for j in range(i + 1, len(matches)):
                        src = matches[i]
                        tgt = matches[j]
                        confidence = self._calculate_match_confidence(
                            src["matched_columns"],
                            tgt["matched_columns"],
                        )
                        if confidence >= min_confidence:
                            mappings.append(CrossReferenceMapping(
                                domain=entity_type,
                                source_system=src["source"],
                                source_key=src["matched_columns"][0],
                                target_system=tgt["source"],
                                target_key=tgt["matched_columns"][0],
                                confidence=confidence,
                                rationale=(
                                    f"Column synonym match: "
                                    f"{src['matched_columns']} ↔ "
                                    f"{tgt['matched_columns']} "
                                    f"(entity type: {entity_type})"
                                ),
                            ))

        duration = time.time() - t0
        self._finish_trace(
            trace,
            f"Found {len(mappings)} cross-reference mappings",
            duration,
        )
        return mappings

    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Runtime entry point — discover cross-references across artifacts."""
        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        mappings = self.analyze(arts)
        return {"cross_ref_mappings": [m.model_dump() for m in mappings]}

    @staticmethod
    def _calculate_match_confidence(
        src_cols: List[str],
        tgt_cols: List[str],
    ) -> float:
        """Calculate confidence based on column name similarity."""
        # Exact match between any pair = high confidence
        if set(src_cols) & set(tgt_cols):
            return 0.95

        # Same synonym group but different names = moderate
        return 0.70


# ---------------------------------------------------------------------------
# P1: QualityPolicyAgent
# ---------------------------------------------------------------------------

@register_agent
class QualityPolicyAgent(BLCEAgent):
    """Generates data quality expectations from logic artifact analysis.

    Infers quality rules based on measure types, filter patterns,
    and column semantics.  Rules follow the Great Expectations style
    (column, rule_type, kwargs, severity).
    """

    agent_name = "QualityPolicyAgent"
    priority = "P1"

    # Aggregation → quality rules
    _AGG_RULES = {
        MeasureAggregation.SUM: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
        ],
        MeasureAggregation.COUNT: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
            {"rule": "expect_column_values_to_be_of_type", "kwargs": {"type_": "INTEGER"},
             "severity": "error"},
        ],
        MeasureAggregation.COUNT_DISTINCT: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
        ],
        MeasureAggregation.AVG: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
            {"rule": "expect_column_values_to_be_of_type", "kwargs": {"type_": "NUMERIC"},
             "severity": "warning"},
        ],
        MeasureAggregation.MIN: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "info"},
        ],
        MeasureAggregation.MAX: [
            {"rule": "expect_column_values_to_not_be_null", "severity": "info"},
        ],
    }

    # Column name patterns → quality rules
    _COLUMN_PATTERNS = {
        r"(?:^|_)id$": [
            {"rule": "expect_column_values_to_be_unique", "severity": "error"},
            {"rule": "expect_column_values_to_not_be_null", "severity": "error"},
        ],
        r"(?:^|_)date(?:$|_)": [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
            {"rule": "expect_column_values_to_be_dateutil_parseable", "severity": "warning"},
        ],
        r"(?:^|_)(?:amount|price|cost|revenue|total)(?:$|_)": [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
            {"rule": "expect_column_values_to_be_of_type", "kwargs": {"type_": "NUMERIC"},
             "severity": "error"},
        ],
        r"(?:^|_)(?:status|state|flag)(?:$|_)": [
            {"rule": "expect_column_values_to_not_be_null", "severity": "warning"},
            {"rule": "expect_column_distinct_values_to_be_in_set", "severity": "info"},
        ],
        r"(?:^|_)(?:email)(?:$|_)": [
            {"rule": "expect_column_values_to_match_regex",
             "kwargs": {"regex": r".+@.+\..+"},
             "severity": "warning"},
        ],
    }

    def analyze(self, artifact: LogicArtifact) -> QualityPolicy:
        """Generate quality expectations for an artifact."""
        t0 = time.time()
        trace = self._create_trace("quality_policy", [artifact.artifact_id])

        expectations = []

        # From measures and their source columns
        for m in artifact.objects.measures:
            rules = self._AGG_RULES.get(m.aggregation, [])
            for col in (m.source_columns or [m.name]):
                if not col:
                    continue
                for rule in rules:
                    exp = {
                        "column": col,
                        "rule": rule["rule"],
                        "severity": rule["severity"],
                        "description": (
                            f"Auto-generated from {m.aggregation.value} "
                            f"measure '{m.alias or m.name}'"
                        ),
                    }
                    if "kwargs" in rule:
                        exp["kwargs"] = rule["kwargs"]
                    expectations.append(exp)

        # From grain columns (should be non-null)
        for g in artifact.objects.grain_columns:
            if g.column:
                expectations.append({
                    "column": g.column,
                    "rule": "expect_column_values_to_not_be_null",
                    "severity": "error",
                    "description": f"Grain column '{g.column}' must not be null",
                })

        # From column name patterns
        all_columns = set()
        for m in artifact.objects.measures:
            all_columns.update(m.source_columns)
        for g in artifact.objects.grain_columns:
            if g.column:
                all_columns.add(g.column)

        for col in all_columns:
            for pattern, rules in self._COLUMN_PATTERNS.items():
                if re.search(pattern, col.lower()):
                    for rule in rules:
                        # Avoid duplicates
                        dup = any(
                            e["column"] == col and e["rule"] == rule["rule"]
                            for e in expectations
                        )
                        if not dup:
                            exp = {
                                "column": col,
                                "rule": rule["rule"],
                                "severity": rule["severity"],
                                "description": (
                                    f"Auto-generated from column pattern "
                                    f"'{pattern}' on '{col}'"
                                ),
                            }
                            if "kwargs" in rule:
                                exp["kwargs"] = rule["kwargs"]
                            expectations.append(exp)

        # Calculate coverage
        total_cols = len(all_columns) or 1
        covered_cols = len({e["column"] for e in expectations})
        coverage = round(covered_cols / total_cols, 2)

        result = QualityPolicy(
            artifact_id=artifact.artifact_id,
            expectations=expectations,
            coverage_score=coverage,
        )

        duration = time.time() - t0
        self._finish_trace(
            trace,
            f"Generated {len(expectations)} expectations, coverage={coverage}",
            duration,
        )
        return result

    def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Runtime entry point — generate quality policies for all artifacts."""
        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        policies = [self.analyze(art).model_dump() for art in arts]
        return {"quality_policies": policies}
