from __future__ import annotations

import re
from pathlib import Path
from typing import Annotated, Literal

from pydantic import AfterValidator, BaseModel, Field, StringConstraints, model_validator
from pydantic_ai import ModelRetry

REQUIRED_IN_BODY = re.compile(r"\bprint\s*\(")

INVALID_IN_BODY: list[tuple[re.Pattern[str], str]] = [
    (
        re.compile(r"def\s+main\s*\("),
        "Não redefina `main()` no body.",
    ),
    (
        re.compile(r"if\s+__name__\s*=="),
        "Não inclua `if __name__` no body.",
    ),
    (
        re.compile(r"^\s*import\s+sys\b", re.MULTILINE),
        "Não importe `sys`; use os parâmetros da main.",
    ),
    (
        re.compile(r"^\s*from\s+sys\s+import", re.MULTILINE),
        "Não importe de `sys`; use os parâmetros da main.",
    ),
    (
        re.compile(r"^\s*import\s+argparse\b", re.MULTILINE),
        "Não importe `argparse`; use os parâmetros da main.",
    ),
    (
        re.compile(r"\beval\s*\("),
        "Não use `eval()`.",
    ),
    (
        re.compile(r"\bexec\s*\("),
        "Não use `exec()`.",
    ),
    (
        re.compile(r"\b__import__\s*\("),
        "Não use `__import__()`.",
    ),
    (
        re.compile(r"=\s*(?:eval|exec|__import__)\b"),
        "Não reatribua builtins sensíveis (`eval`, `exec`, `__import__`).",
    ),
]


def validate_script_body(v: str) -> str:
    for pattern, message in INVALID_IN_BODY:
        if pattern.search(v):
            raise ModelRetry(f"Script body inválido: {message}")
    if not REQUIRED_IN_BODY.search(v):
        raise ModelRetry("Script body inválido: deve imprimir algo no stdout (use `print()`).")
    return v.strip()


NonEmptyString = Annotated[
    str,
    StringConstraints(
        min_length=1,
        strip_whitespace=True,
    ),
]

KebabCase = Annotated[
    str,
    StringConstraints(
        pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$",
        strip_whitespace=True,
    ),
]

SkillScriptFileName = Annotated[
    NonEmptyString,
    StringConstraints(
        pattern=r"^[^/\\\s]+\.py$",
        strip_whitespace=True,
    ),
]

SkillScriptArgumentName = Annotated[
    NonEmptyString,
    StringConstraints(
        pattern=r"^[a-z][a-z0-9_]*$",
        strip_whitespace=True,
    ),
]

SkillScriptArgumentType = str | int | float | bool
SkillScriptArgumentTypeAsString = Literal[
    "str",
    "int",
    "float",
    "bool",
]

SkillScriptBody = Annotated[
    NonEmptyString,
    AfterValidator(validate_script_body),
]


class SkillScriptArgument(BaseModel):
    name: SkillScriptArgumentName
    type: SkillScriptArgumentTypeAsString
    description: NonEmptyString
    required: bool = True
    default: SkillScriptArgumentType | None = None

    @model_validator(mode="after")
    def validate_default_not_set_when_required(self) -> SkillScriptArgument:
        if self.required and self.default is not None:
            raise ValueError(
                f"Argumento '{self.name}': required=True não pode ter default. "
                "Defina required=False ou remova o default."
            )
        return self


class SkillScriptDraft(BaseModel):
    file_name: SkillScriptFileName
    when_to_use: NonEmptyString
    requires_python: str = Field(default=">=3.13,<3.14", min_length=4)
    dependencies: set[NonEmptyString] = Field(default_factory=set)
    args: list[SkillScriptArgument] = Field(default_factory=list)


class SkillScript(SkillScriptDraft):
    body: SkillScriptBody


class SkillSpec(BaseModel):
    name: KebabCase = Field(min_length=1, max_length=64)
    description: NonEmptyString = Field(max_length=1024)
    instructions: NonEmptyString
    scripts: list[SkillScript] = Field(default_factory=list)

    @model_validator(mode="after")
    def validate_unique_filenames(self) -> SkillSpec:
        seen: set[str] = set()
        for script in self.scripts:
            normalized = script.file_name.casefold()
            if normalized in seen:
                raise ModelRetry(
                    f"scripts contém nomes de arquivo duplicados: {script.file_name!r}."
                )
            seen.add(normalized)
        return self


class SkillDraft(BaseModel):
    name: str
    description: str
    scripts: list[SkillScript] = Field(default_factory=list)


class SkillCreationDeps(BaseModel):
    is_update: bool = False
    skills_directory: Path
    skill_draft: SkillDraft
    current_script: SkillScriptDraft | None = None
    concluded: bool = False


class CreatedSkill(BaseModel):
    name: str
    skill_directory: Path
    created_files: list[str]


CreationStage = Literal["PLANNING", "SCRIPTING", "CONCLUDED"]
