# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Validation endpoints for data recipe."""

from __future__ import annotations

from typing import Any

from fastapi import APIRouter

from core.data_recipe.service import (
    build_config_builder,
    create_data_designer,
    validate_recipe,
)
from loggers import get_logger
from models.data_recipe import RecipePayload, ValidateError, ValidateResponse
from utils.utils import safe_error_detail, safe_curated_detail, log_and_http_error

logger = get_logger(__name__)
router = APIRouter()

_GITHUB_VALIDATE_NOTE = (
    "Recipe shape is valid. GitHub access and rate limits are checked when the run starts."
)
_GITHUB_ITEM_TYPES = {"issues", "pulls", "commits"}


def _github_seed_source(recipe: dict[str, Any]) -> dict[str, Any] | None:
    seed_config = recipe.get("seed_config")
    if not isinstance(seed_config, dict):
        return None
    source = seed_config.get("source")
    if not isinstance(source, dict) or source.get("seed_type") != "github_repo":
        return None
    return source


def _validate_github_seed_static(source: dict[str, Any]) -> list[ValidateError]:
    errors: list[ValidateError] = []

    repos = source.get("repos")
    if not isinstance(repos, list) or not repos:
        errors.append(ValidateError(message = "GitHub seed requires at least one repo."))
    else:
        for repo in repos:
            if not isinstance(repo, str) or not repo.strip() or "/" not in repo:
                errors.append(ValidateError(message = "GitHub repos must be owner/name strings."))
                break

    item_types = source.get("item_types")
    if not isinstance(item_types, list) or not item_types:
        errors.append(ValidateError(message = "GitHub seed requires at least one item type."))
    else:
        invalid_items = [item for item in item_types if item not in _GITHUB_ITEM_TYPES]
        if invalid_items:
            errors.append(
                ValidateError(message = "GitHub item types must be issues, pulls, or commits.")
            )

    try:
        limit = int(source.get("limit"))
    except (TypeError, ValueError):
        limit = 0
    if limit < 1 or limit > 5000:
        errors.append(ValidateError(message = "GitHub limit must be from 1 to 5000."))

    return errors


def _collect_validation_errors(recipe: dict[str, Any]) -> list[ValidateError]:
    try:
        from data_designer.engine.compiler import (
            _add_internal_row_id_column_if_needed,
            _get_allowed_references,
            _resolve_and_add_seed_columns,
        )
        from data_designer.engine.validation import (
            ViolationLevel,
            validate_data_designer_config,
        )
    except ImportError:
        return []

    try:
        builder = build_config_builder(recipe)
        designer = create_data_designer(recipe)
        resource_provider = designer._create_resource_provider(  # type: ignore[attr-defined]
            "validate-configuration",
            builder,
        )
        config = builder.build()
        _resolve_and_add_seed_columns(config, resource_provider.seed_reader)
        _add_internal_row_id_column_if_needed(config)
        violations = validate_data_designer_config(
            columns = config.columns,
            processor_configs = config.processors or [],
            allowed_references = _get_allowed_references(config),
        )
    except (TypeError, ValueError, AttributeError):
        return []

    errors: list[ValidateError] = []
    for violation in violations:
        if violation.level != ViolationLevel.ERROR:
            continue
        code = getattr(violation.type, "value", None)
        path = violation.column if violation.column else None
        message = str(violation.message).strip() or "Validation failed."
        errors.append(
            ValidateError(
                message = message,
                path = path,
                code = code,
            )
        )
    return errors


def _patch_local_providers(recipe: dict[str, Any]) -> None:
    """Strip is_local and fill a dummy endpoint so validation doesn't choke.

    Strict `is True` matches _inject_local_providers: truthy non-boolean values
    aren't treated as local.
    """
    for provider in recipe.get("model_providers", []):
        if not isinstance(provider, dict):
            continue
        if provider.pop("is_local", None) is True:
            provider["endpoint"] = "http://127.0.0.1"


@router.post("/validate", response_model = ValidateResponse)
def validate(payload: RecipePayload) -> ValidateResponse:
    recipe = payload.recipe
    if not recipe.get("columns"):
        return ValidateResponse(
            valid = False,
            errors = [ValidateError(message = "Recipe must include columns.")],
        )

    _patch_local_providers(recipe)

    github_source = _github_seed_source(recipe)
    if github_source is not None:
        static_errors = _validate_github_seed_static(github_source)
        if static_errors:
            return ValidateResponse(valid = False, errors = static_errors)
        try:
            build_config_builder(recipe)
        except ModuleNotFoundError as exc:
            # data_designer is an optional runtime dep; static validation passed
            # and full validation is deferred to run start, so a missing import
            # shouldn't block the recipe. Restrict the bypass to data_designer so
            # other ImportErrors still surface as failures.
            if not (exc.name or "").startswith("data_designer"):
                raise
            logger.debug(
                "data_designer not installed; deferring full config validation to run start",
                missing_module = exc.name,
            )
        except Exception as exc:
            logger.error(
                "data_recipe.validate.github_config_failed",
                error = str(exc),
                exc_info = True,
            )
            detail = safe_error_detail(exc, fallback = "Validation failed.")
            return ValidateResponse(
                valid = False,
                errors = [ValidateError(message = detail)],
                raw_detail = detail,
            )
        return ValidateResponse(valid = True, raw_detail = _GITHUB_VALIDATE_NOTE)

    try:
        validate_recipe(recipe)
    except RuntimeError as exc:
        raise log_and_http_error(
            exc,
            503,
            safe_error_detail(exc),
            event = "data_recipe.validate.service_unavailable",
            log = logger,
        ) from exc
    except Exception as exc:
        logger.error(
            "data_recipe.validate.recipe_failed",
            error = str(exc),
            exc_info = True,
        )
        detail = safe_curated_detail(exc, fallback = "Validation failed.")
        parsed_errors = _collect_validation_errors(recipe)
        return ValidateResponse(
            valid = False,
            errors = parsed_errors or [ValidateError(message = detail)],
            raw_detail = detail,
        )

    return ValidateResponse(valid = True)
