"""Static validation for loony projects — instant, offline, no DB needed."""

import ast
from dataclasses import dataclass, field
from pathlib import Path

import yaml

VALID_TYPES = {
    "integer",
    "bigint",
    "smallint",
    "serial",
    "bigserial",
    "text",
    "varchar",
    "char",
    "boolean",
    "numeric",
    "decimal",
    "real",
    "double precision",
    "float",
    "timestamptz",
    "timestamp",
    "date",
    "time",
    "interval",
    "jsonb",
    "json",
    "vector",
    "uuid",
    "bytea",
}


@dataclass
class Check:
    ok: bool
    label: str
    detail: str = ""
    errors: list[str] = field(default_factory=list)


@dataclass
class ValidationResult:
    name: str
    checks: list[Check] = field(default_factory=list)
    files: dict[str, str] = field(default_factory=dict)

    @property
    def passed(self) -> bool:
        return all(c.ok for c in self.checks)


def validate_service(service_dir: Path) -> ValidationResult:
    """Validate a loony project. Returns structured result, prints nothing."""
    result = ValidationResult(name="")

    # 1. loony.yaml
    loony_yaml_path = service_dir / "loony.yaml"
    if not loony_yaml_path.exists():
        result.checks.append(Check(ok=False, label="loony.yaml", errors=["File not found"]))
        return result

    try:
        loony_conf = yaml.safe_load(loony_yaml_path.read_text())
        name = loony_conf.get("name", "")
        if not name:
            result.checks.append(Check(ok=False, label="loony.yaml", errors=["Missing 'name' field"]))
            return result
        result.name = name
        result.checks.append(Check(ok=True, label="loony.yaml", detail=f"name: {name}"))
    except yaml.YAMLError as e:
        result.checks.append(Check(ok=False, label="loony.yaml", errors=[f"Parse error: {e}"]))
        return result

    # 2. schema.yaml
    schema_path = service_dir / "schema.yaml"
    if not schema_path.exists():
        result.checks.append(Check(ok=False, label="schema.yaml", errors=["File not found"]))
        return result

    try:
        schema = yaml.safe_load(schema_path.read_text())
        tables = schema.get("tables", {})
        if not tables:
            result.checks.append(Check(ok=False, label="schema.yaml", errors=["No tables declared"]))
            return result
        result.checks.append(Check(ok=True, label="schema.yaml", detail=f"{len(tables)} tables"))
    except yaml.YAMLError as e:
        result.checks.append(Check(ok=False, label="schema.yaml", errors=[f"Parse error: {e}"]))
        return result

    # 3. sync_mode + primary_key on tables that declare them
    schema_errors = []
    for table_name, table_def in tables.items():
        if not isinstance(table_def, dict):
            continue
        sync_mode = table_def.get("sync_mode")
        if sync_mode and sync_mode not in ("replace", "merge", "append"):
            schema_errors.append(f"{table_name} — invalid sync_mode '{sync_mode}' (must be replace, merge, or append)")
        if sync_mode == "merge" and not table_def.get("primary_key"):
            schema_errors.append(f"{table_name} — merge mode requires primary_key")

        # Validate column types
        columns = table_def.get("columns", {})
        for col_name, col_def in columns.items():
            if isinstance(col_def, dict):
                col_type = col_def.get("type", "")
                if col_type and col_type not in VALID_TYPES:
                    schema_errors.append(f"{table_name}.{col_name} — unknown type '{col_type}'")

    if schema_errors:
        result.checks.append(Check(ok=False, label="schema contract", errors=schema_errors))
    else:
        result.checks.append(Check(ok=True, label="schema contract", detail="sync_mode + types valid"))

    # 4. Scripts — check for def run( using AST
    scripts_dir = service_dir / "scripts"
    script_files = sorted(scripts_dir.glob("*.py")) if scripts_dir.exists() else []
    script_files = [f for f in script_files if f.name != "__init__.py"]

    if not script_files:
        result.checks.append(Check(ok=True, label="scripts", detail="none (transforms only)"))
    else:
        script_errors = []
        for script_path in script_files:
            try:
                tree = ast.parse(script_path.read_text())
                has_run = any(
                    isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "run"
                    for node in ast.iter_child_nodes(tree)
                )
                if not has_run:
                    script_errors.append(f"{script_path.name} — no run() function found")
            except SyntaxError as e:
                script_errors.append(f"{script_path.name} — syntax error: {e}")

        if script_errors:
            result.checks.append(Check(ok=False, label="scripts", errors=script_errors))
        else:
            result.checks.append(
                Check(ok=True, label="scripts", detail=f"{len(script_files)} scripts, all export run(conn)")
            )

    # 5. Transforms (optional)
    transforms_dir = service_dir / "transforms"
    sql_files = sorted(transforms_dir.glob("*.sql")) if transforms_dir.exists() else []

    if sql_files:
        result.checks.append(Check(ok=True, label="transforms", detail=f"{len(sql_files)} SQL files"))
    # No transforms is fine — not an error

    # 6. Collect files for upload
    result.files["loony.yaml"] = loony_yaml_path.read_text()
    result.files["schema.yaml"] = schema_path.read_text()

    for f in script_files:
        result.files[f"scripts/{f.name}"] = f.read_text()

    for f in sql_files:
        result.files[f"transforms/{f.name}"] = f.read_text()

    req_path = service_dir / "requirements.txt"
    if req_path.exists():
        result.files["requirements.txt"] = req_path.read_text()

    return result
