"""Parsea y valida recipe YAML del navegator."""

import re


def validate_recipe_yaml(yaml_text: str) -> dict:
    """Parsea + valida recipe YAML del navegator.

    Args:
        yaml_text: Texto YAML de la recipe.

    Returns:
        {valid: bool, errors: [str], warnings: [str], parsed: dict}
    """
    try:
        import yaml
    except ImportError:
        return {
            "valid": False,
            "errors": ["pyyaml no disponible: instalar con 'pip install pyyaml'"],
            "warnings": [],
            "parsed": {},
        }

    errors = []
    warnings = []
    parsed = {}

    try:
        parsed = yaml.safe_load(yaml_text) or {}
    except yaml.YAMLError as e:
        return {
            "valid": False,
            "errors": [f"YAML invalido: {e}"],
            "warnings": [],
            "parsed": {},
        }

    if not isinstance(parsed, dict):
        return {
            "valid": False,
            "errors": ["El YAML debe ser un objeto (dict) en el nivel raiz."],
            "warnings": [],
            "parsed": parsed,
        }

    # name obligatorio, snake_case
    name = parsed.get("name")
    if not name:
        errors.append("Campo 'name' obligatorio.")
    elif not isinstance(name, str):
        errors.append("Campo 'name' debe ser string.")
    elif not re.fullmatch(r"[a-z][a-z0-9_]*", name):
        errors.append(f"Campo 'name' debe ser snake_case (solo [a-z0-9_], empieza con letra): '{name}'.")

    # url_pattern obligatorio, regex valido
    url_pattern = parsed.get("url_pattern")
    if not url_pattern:
        errors.append("Campo 'url_pattern' obligatorio.")
    elif not isinstance(url_pattern, str):
        errors.append("Campo 'url_pattern' debe ser string.")
    else:
        try:
            re.compile(url_pattern)
        except re.error as e:
            errors.append(f"Campo 'url_pattern' no es un regex valido: {e}")

    # steps lista no vacia, cada step requiere js O wait_selector
    steps = parsed.get("steps")
    if not steps:
        errors.append("Campo 'steps' obligatorio y no puede estar vacio.")
    elif not isinstance(steps, list):
        errors.append("Campo 'steps' debe ser una lista.")
    else:
        for i, step in enumerate(steps):
            if not isinstance(step, dict):
                errors.append(f"Step {i}: debe ser un objeto.")
                continue
            has_js = "js" in step
            has_wait = "wait_selector" in step
            if not has_js and not has_wait:
                errors.append(f"Step {i}: requiere 'js' o 'wait_selector'.")
            if has_js and has_wait:
                warnings.append(f"Step {i}: tiene tanto 'js' como 'wait_selector'; se usara 'js'.")

    # output opcional pero validado si presente
    output = parsed.get("output")
    if output is not None:
        if not isinstance(output, dict):
            errors.append("Campo 'output' debe ser un objeto.")
        else:
            schema = output.get("schema")
            if schema is not None:
                if not isinstance(schema, list):
                    errors.append("Campo 'output.schema' debe ser una lista.")
                else:
                    valid_types = {"string", "int", "float", "bool", "date"}
                    for j, item in enumerate(schema):
                        if not isinstance(item, dict):
                            errors.append(f"output.schema[{j}]: debe ser un objeto.")
                            continue
                        if "field" not in item:
                            errors.append(f"output.schema[{j}]: falta campo 'field'.")
                        if "type" not in item:
                            errors.append(f"output.schema[{j}]: falta campo 'type'.")
                        elif item["type"] not in valid_types:
                            warnings.append(
                                f"output.schema[{j}]: tipo '{item['type']}' no reconocido "
                                f"(esperado: {sorted(valid_types)})."
                            )

            sink = output.get("sink")
            # duckdb sink: requires output.duckdb_path (relative or absolute) and
            # output.table (table name). Optional output.database_id (default =
            # recipe_name + "_db") used to register/lookup in data_factory.databases.
            valid_sinks = {"data_factory.runs", "stdout", "json_file", "duckdb"}
            if sink is not None and sink not in valid_sinks:
                errors.append(
                    f"Campo 'output.sink' debe ser uno de {sorted(valid_sinks)}, got '{sink}'."
                )
            if sink == "duckdb":
                if not output.get("duckdb_path"):
                    errors.append(
                        "Sink 'duckdb' requiere 'output.duckdb_path' (ruta al archivo .duckdb)."
                    )
                if not output.get("table"):
                    errors.append(
                        "Sink 'duckdb' requiere 'output.table' (nombre de la tabla destino)."
                    )

    return {
        "valid": len(errors) == 0,
        "errors": errors,
        "warnings": warnings,
        "parsed": parsed,
    }
