"""Pydantic-SQLAlchemy Field Mismatch Checker.

Detects when a Pydantic schema with `from_attributes = True` (ORM mode) references
fields that don't exist in the corresponding SQLAlchemy model.

Example bug this catches:
    # TikTokAdCreative has `ad_status: Optional[str] = None`
    # But TikTokAdMetadata uses `operation_status`, not `ad_status`
    # Result: 500 error when trying to access ad.ad_status

This checker only validates schemas that:
1. Inherit from BaseModel
2. Have `from_attributes = True` in their Config (indicating ORM mode)
3. Have a corresponding SQLAlchemy model (matched by naming convention)
"""

import ast
from pathlib import Path
from typing import Dict, List, Optional

from stablestack.checkers.base import BaseChecker
from stablestack.models import Finding, Severity, Tier


class PydanticSqlalchemyFieldChecker(BaseChecker):
    """Checks for field mismatches between Pydantic ORM schemas and SQLAlchemy models."""

    rule_id = "SCHEMA002"
    rule_name = "pydantic-sqlalchemy-field-mismatch"
    description = "Pydantic ORM schema references field that doesn't exist in SQLAlchemy model."
    why = (
        "When a Pydantic schema with `from_attributes = True` references a field that doesn't "
        "exist in the SQLAlchemy model, you get a runtime AttributeError (usually a 500 error). "
        "Pydantic's ORM mode accesses model attributes by name, so field names must match exactly. "
        "Either rename the Pydantic field, add an alias, or map manually in the API layer."
    )
    severity = Severity.ERROR
    tier = Tier.CRITICAL
    file_extensions = set()  # Project-level checker
    is_project_checker = True

    # Fields that are commonly added by Pydantic but don't come from DB
    SYNTHETIC_FIELDS = {"model_config"}

    # Patterns that indicate computed/aggregated schemas (NOT direct ORM mappings)
    COMPUTED_PATTERNS = [
        "Stats", "Summary", "Aggregate", "Count", "Total", "List", "Download"
    ]

    def check_file(self, path: Path, content: str) -> List[Finding]:  # noqa: ARG002
        """Not used for project checker."""
        return []

    def check_project(self, project_root: Path) -> List[Finding]:
        """Run field mismatch checks across the project."""
        findings: List[Finding] = []

        # Try to find backend directory
        backend_dirs = [
            project_root / "backend",
            project_root / "src",
            project_root / "app",
            project_root,
        ]

        backend_dir = None
        for bd in backend_dirs:
            if (bd / "models").exists() or (bd / "api").exists():
                backend_dir = bd
                break

        if backend_dir is None:
            return findings

        # Extract SQLAlchemy model fields
        sqlalchemy_models = self._extract_sqlalchemy_models(backend_dir)

        if not sqlalchemy_models:
            return findings

        # Check Pydantic ORM schemas against SQLAlchemy models
        findings.extend(
            self._check_pydantic_schemas(sqlalchemy_models, backend_dir, project_root)
        )

        return findings

    # ── Extraction helpers ──────────────────────────────────────────────

    def _extract_sqlalchemy_models(
        self, backend_dir: Path,
    ) -> Dict[str, Dict[str, int]]:
        """Extract SQLAlchemy model names and their field names + line numbers.

        Returns {ModelName: {field_name: line_number}}
        """
        models: Dict[str, Dict[str, int]] = {}

        for py_file in backend_dir.rglob("*.py"):
            try:
                source = py_file.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                continue

            if "Column" not in source and "mapped_column" not in source:
                continue

            try:
                tree = ast.parse(source)
            except SyntaxError:
                continue

            for node in ast.walk(tree):
                if not isinstance(node, ast.ClassDef):
                    continue

                base_names = [
                    getattr(b, "id", getattr(b, "attr", ""))
                    for b in node.bases
                ]
                is_sqla = any(
                    "Base" in n or "Model" in n or "Mixin" in n
                    for n in base_names
                )
                if not is_sqla:
                    continue

                fields: Dict[str, int] = {}
                for item in node.body:
                    field_name: Optional[str] = None
                    value = None

                    if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
                        field_name = item.target.id
                        value = item.value
                    elif isinstance(item, ast.Assign) and len(item.targets) == 1:
                        target = item.targets[0]
                        if isinstance(target, ast.Name):
                            field_name = target.id
                        value = item.value

                    if field_name is None or field_name.startswith("_"):
                        continue
                    if field_name == "__tablename__":
                        continue

                    # Verify it's a Column/mapped_column call
                    if isinstance(value, ast.Call):
                        func_name = ""
                        if isinstance(value.func, ast.Name):
                            func_name = value.func.id
                        elif isinstance(value.func, ast.Attribute):
                            func_name = value.func.attr
                        if func_name in ("Column", "mapped_column", "relationship"):
                            fields[field_name] = item.lineno

                if fields:
                    models[node.name] = fields

        return models

    # ── Schema checking ─────────────────────────────────────────────────

    def _check_pydantic_schemas(
        self,
        sqlalchemy_models: Dict[str, Dict[str, int]],
        backend_dir: Path,
        project_root: Path,
    ) -> List[Finding]:
        """Find Pydantic ORM schemas and check their fields against SQLAlchemy models."""
        findings: List[Finding] = []

        search_dirs = [backend_dir]
        if project_root != backend_dir:
            search_dirs.append(project_root)

        for search_dir in search_dirs:
            for py_file in search_dir.rglob("*.py"):
                try:
                    source = py_file.read_text(encoding="utf-8")
                except (OSError, UnicodeDecodeError):
                    continue

                if "from_attributes" not in source and "orm_mode" not in source:
                    continue

                try:
                    tree = ast.parse(source)
                except SyntaxError:
                    continue

                for node in ast.walk(tree):
                    if not isinstance(node, ast.ClassDef):
                        continue

                    # Check if this is a Pydantic model
                    base_names = [
                        getattr(b, "id", getattr(b, "attr", ""))
                        for b in node.bases
                    ]
                    is_pydantic = any(
                        "Model" in n or "Schema" in n or "Response" in n
                        for n in base_names
                    )
                    if not is_pydantic:
                        continue

                    # Check for from_attributes = True or orm_mode = True
                    if not self._has_orm_mode(node):
                        continue

                    # Skip computed/aggregated schemas
                    if any(p in node.name for p in self.COMPUTED_PATTERNS):
                        continue

                    # Extract Pydantic field names
                    pydantic_fields: Dict[str, int] = {}
                    for item in node.body:
                        if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
                            fname = item.target.id
                            if not fname.startswith("_") and fname not in self.SYNTHETIC_FIELDS:
                                pydantic_fields[fname] = item.lineno

                    if not pydantic_fields:
                        continue

                    # Try to match to a SQLAlchemy model
                    base_name = (
                        node.name
                        .replace("Response", "")
                        .replace("Schema", "")
                        .replace("Out", "")
                        .replace("Read", "")
                        .replace("Create", "")
                        .replace("Update", "")
                    )

                    sqla_model = sqlalchemy_models.get(base_name)
                    if sqla_model is None:
                        continue

                    sqla_field_names = set(sqla_model.keys())

                    # Check each Pydantic field exists in the SQLAlchemy model
                    for field_name, field_line in pydantic_fields.items():
                        if field_name not in sqla_field_names:
                            findings.append(
                                self.create_finding(
                                    path=py_file,
                                    line=field_line,
                                    column=1,
                                    code=f"{field_name}: ... (in {node.name})",
                                    message=(
                                        f"'{node.name}.{field_name}' has from_attributes=True "
                                        f"but '{base_name}' has no '{field_name}' column. "
                                        f"This will cause AttributeError at runtime."
                                    ),
                                    fix_hint=(
                                        f"Either add '{field_name}' to the {base_name} model, "
                                        f"rename the Pydantic field to match, or compute it "
                                        f"manually in the API layer instead of relying on ORM mode."
                                    ),
                                )
                            )

        return findings

    @staticmethod
    def _has_orm_mode(class_node: ast.ClassDef) -> bool:
        """Check if a Pydantic class has from_attributes=True or orm_mode=True."""
        for item in class_node.body:
            # model_config = ConfigDict(from_attributes=True)
            if isinstance(item, ast.Assign):
                for target in item.targets:
                    if isinstance(target, ast.Name) and target.id == "model_config":
                        if isinstance(item.value, ast.Call):
                            for kw in item.value.keywords:
                                if kw.arg in ("from_attributes", "orm_mode"):
                                    if isinstance(kw.value, ast.Constant) and kw.value.value is True:
                                        return True

            # Inner Config class with orm_mode = True or from_attributes = True
            if isinstance(item, ast.ClassDef) and item.name in ("Config", "model_config"):
                for sub_item in item.body:
                    if isinstance(sub_item, ast.Assign):
                        for t in sub_item.targets:
                            if isinstance(t, ast.Name) and t.id in ("from_attributes", "orm_mode"):
                                if isinstance(sub_item.value, ast.Constant) and sub_item.value.value is True:
                                    return True

        return False
