"""Pydantic models for request/response validation."""

import json
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional

import structlog
from pydantic import BaseModel, ConfigDict, Field, field_validator

from rasa.cli.scaffold import ProjectTemplateName
from rasa.shared.importers.importer import TrainingDataImporter

structlogger = structlog.get_logger()


class PromptRequest(BaseModel):
    """Request model for prompt-to-bot endpoint."""

    prompt: str = Field(
        ..., min_length=1, max_length=10000, description="The skill description prompt"
    )

    @field_validator("prompt")
    @classmethod
    def validate_prompt(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Prompt cannot be empty or whitespace only")
        return v.strip()


class TemplateRequest(BaseModel):
    """Request model for template-to-bot endpoint."""

    template_name: ProjectTemplateName = Field(
        ...,
        description=(
            f"The template name to use ({ProjectTemplateName.supported_values()})"
        ),
    )

    @field_validator("template_name")
    @classmethod
    def validate_template_name(cls, v: Any) -> Any:
        if v not in ProjectTemplateName:
            raise ValueError(
                f"Template name must be one of {ProjectTemplateName.supported_values()}"
            )
        return v


class ChangeBranchRequest(BaseModel):
    """Request model for change branch endpoint."""

    branch_name: str = Field(
        ..., min_length=1, max_length=200, description="The branch name to checkout"
    )
    create_if_not_exists: bool = Field(
        False, description="Whether to create the branch if it doesn't exist"
    )

    @field_validator("branch_name")
    @classmethod
    def validate_branch_name(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Branch name cannot be empty or whitespace only")
        v = v.strip()
        # Basic validation for branch names
        if any(char in v for char in [" ", "\t", "\n", "\r"]):
            raise ValueError("Branch name cannot contain whitespace")
        return v


class GitStatusResponse(BaseModel):
    """Response model for Git status information."""

    current_branch: str = Field(..., description="Current Git branch")
    uncommitted_changes: bool = Field(
        ..., description="Whether there are uncommitted changes"
    )


class GitCommitInfo(BaseModel):
    """Author and message for a commit."""

    author: str = Field(..., description="Author of the commit")
    email: str = Field(..., description="Email of the author")
    message: Optional[str] = Field(None, description="Message of the commit")


class GitHubToBotRequest(BaseModel):
    """Request model for github-to-bot endpoint.

    This endpoint is used to initialize the bot from a public GitHub repository.
    """

    repo_url: str = Field(
        ...,
        min_length=1,
        description="HTTPS URL of the public GitHub repository to clone.",
    )
    branch: Optional[str] = Field(
        None,
        description="Branch to checkout. Defaults to the repository's default branch.",
    )

    @field_validator("repo_url")
    @classmethod
    def validate_repo_url(cls, v: str) -> str:
        """Validate that the repository URL is a valid HTTPS GitHub URL.

        This prevents command injection and SSRF attacks by ensuring only
        valid GitHub.com URLs are accepted. URLs must match the exact pattern
        https://github.com/owner/repo[.git] - no other domains are allowed.
        """
        import re

        v = v.strip()
        # Remove trailing slash if present
        # (for convenience when copying URLs from browser)
        v = v.rstrip("/")

        # Must be HTTPS
        if not v.startswith("https://"):
            raise ValueError("Repository URL must be an HTTPS URL")

        # SECURITY: Only accept github.com URLs to prevent SSRF attacks
        # The pattern explicitly requires "github.com" (not subdomains or other domains)
        # Matches: https://github.com/owner/repo or https://github.com/owner/repo.git
        # Rejects: https://evil.com/*, https://internal-git/*, etc.
        github_pattern = r"^https://github\.com/[\w\-\.]+/[\w\-\.]+(?:\.git)?$"
        if not re.match(github_pattern, v):
            raise ValueError(
                "Repository URL must be a valid GitHub URL in the format: "
                "https://github.com/owner/repo or https://github.com/owner/repo.git"
            )

        return v

    @field_validator("branch")
    @classmethod
    def validate_branch(cls, v: Optional[str]) -> Optional[str]:
        """Validate branch name to prevent command injection.

        Git branch names have specific rules. This validator ensures the branch
        name doesn't contain shell metacharacters or other dangerous content.
        """
        import re

        if v is None:
            return v

        v = v.strip()

        # Empty branch name is not allowed
        if not v:
            return None

        # Branch names cannot contain: space, ~, ^, :, ?, *, [, \, .., @{
        # They cannot start with . or end with .lock or /
        # This pattern allows alphanumeric, dash, underscore, dot, and forward slash
        branch_pattern = r"^[a-zA-Z0-9][\w\-\./]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$"
        if not re.match(branch_pattern, v):
            raise ValueError(
                "Branch name can only contain alphanumeric characters, "
                "hyphens, underscores, dots, and forward slashes"
            )

        # Additional checks for dangerous patterns
        dangerous_patterns = ["..", "@{", " ", "~", "^", ":", "?", "*", "[", "\\"]
        for pattern in dangerous_patterns:
            if pattern in v:
                raise ValueError(f"Branch name cannot contain '{pattern}'")

        if v.startswith(".") or v.endswith(".lock") or v.endswith("/"):
            raise ValueError("Invalid branch name format")

        return v


class CommitFileContents(BaseModel):
    """Original and modified contents of a file in a commit."""

    status: str = Field(..., description="Status of the file")
    content_original: str | None = Field(
        None, description="Original file content. None if file is binary."
    )
    content_modified: str | None = Field(
        None, description="Modified file content. None if file is binary."
    )
    path_original: str | None = Field(
        None, description="Original file path (Only for renamed files)"
    )
    path_modified: str | None = Field(
        None, description="Modified file path (Only for renamed files)"
    )

    @field_validator("status")
    @classmethod
    def validate_status(cls, v: str) -> str:
        if v not in ["R", "A", "M", "D"]:
            raise ValueError(f"Invalid status: {v}, must be one of R, A, M, D")
        return v


class CommitDiffWithContentsResponse(BaseModel):
    """Response model for commit diff with file contents."""

    files: Dict[str, CommitFileContents] = Field(
        ...,
        description=(
            "Dictionary mapping file paths to their original and " "modified contents"
        ),
    )


class RestoreFromBackupRequest(BaseModel):
    """Request model for backup-to-bot endpoint."""

    presigned_url: str = Field(
        ...,
        min_length=1,
        description="Presigned URL to download tar.gz backup file.",
    )

    @field_validator("presigned_url")
    @classmethod
    def validate_presigned_url(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Presigned URL cannot be empty or whitespace only")

        # Basic URL validation
        url = v.strip()
        if not url.startswith(("http://", "https://")):
            raise ValueError("Presigned URL must be a valid HTTP/HTTPS URL")

        return url


class BotDataUpdateRequest(BaseModel):
    """Request model for bot data updates."""

    domain_yml: Optional[str] = Field(None, alias="domain.yml")
    flows_yml: Optional[str] = Field(None, alias="flows.yml")
    config_yml: Optional[str] = Field(None, alias="config.yml")

    # Allow using either field names or aliases when creating the model
    model_config = ConfigDict(populate_by_name=True)


class BotData(BaseModel):
    """Data of a running assistant."""

    domain: Dict[str, Any] = Field(..., description="The domain of the assistant.")
    flows: Dict[str, Any] = Field(..., description="The flows of the assistant.")


class AssistantInfo(BaseModel):
    """Basic information about the loaded assistant."""

    assistant_id: Optional[str] = Field(
        None,
        description=(
            "Assistant identifier coming from `assistant_id` in the model "
            "metadata (config.yml)."
        ),
    )


class ApiErrorResponse(BaseModel):
    """API error response model."""

    status: Literal["error"] = "error"
    error: str = Field(...)
    details: Optional[Dict[str, Any]] = Field(None)


class ServerSentEventType(str, Enum):
    progress = "progress"
    error = "error"
    _EOF = "_EOF"


class ServerSentEvent(BaseModel):
    """Generic Server-Sent Event payload."""

    event: str = Field(..., description="SSE event name / type")
    data: Dict[str, Any] = Field(
        default_factory=dict,
        description="Arbitrary JSON-serializable payload",
    )

    @classmethod
    def build(cls, event: str, data: Any) -> "ServerSentEvent":
        """General-purpose constructor.

        Args:
            event: The event name (e.g. "progress", "error").
            data: Arbitrary key-value pairs to include in the payload.

        Returns:
            A ServerSentEvent instance with the specified event and data.
        """
        return cls(event=event, data=data)

    @classmethod
    def eof(cls) -> "ServerSentEvent":
        """Helper that returns the special end-of-stream marker."""
        return cls(event=ServerSentEventType._EOF.value, data={})

    def format(self) -> str:
        """Return the text representation used by SSE protocols."""
        return (
            f"event: {self.event}\n"
            f"data: {json.dumps(self.data, separators=(', ', ': '))}\n\n"
        )


class JobStatusEvent(ServerSentEvent):
    """Job status event with special handling for progress and error states."""

    @classmethod
    def from_status(
        cls,
        status: str,
        message: Optional[str] = None,
        payload: Optional[Dict[str, Any]] = None,
    ) -> "JobStatusEvent":
        """Factory for job-status events.

        Args:
            status: The job status (e.g. "training", "train_success").
            message: Optional error message for error events.
            payload: Optional additional payload data to include in the event.

        Returns:
            A JobStatusEvent instance with the appropriate event type and data.
        """
        event_type = (
            ServerSentEventType.error if message else ServerSentEventType.progress
        )

        event_payload: Dict[str, Any] = {"status": status}
        if message:
            event_payload["message"] = message
        if payload:
            event_payload.update(payload)

        return cls(event=event_type.value, data=event_payload)


class ValidationResult(BaseModel):
    """Result of validation operation."""

    is_valid: bool = Field(...)
    errors: Optional[List[str]] = Field(None)
    warnings: Optional[List[str]] = Field(None)


class TrainingResult(BaseModel):
    """Result of training operation."""

    success: bool = Field(...)
    model_path: Optional[str] = Field(None)
    error: Optional[str] = Field(None)


BotFiles = Dict[str, Optional[str]]


class JobStatus(str, Enum):
    received = "received"
    done = "done"
    error = "error"
    commit = "commit"

    heartbeat = "heartbeat"
    generating = "generating"
    generation_success = "generation_success"
    generation_error = "generation_error"

    training = "training"
    train_success = "train_success"
    train_success_message = "train_success_message"
    train_error = "train_error"

    validating = "validating"
    validation_success = "validation_success"
    validation_error = "validation_error"

    # Git-specific statuses
    cloning = "cloning"
    clone_success = "clone_success"
    clone_error = "clone_error"

    switching_branch = "switching_branch"
    branch_switch_success = "branch_switch_success"
    branch_switch_error = "branch_switch_error"

    rolling_back = "rolling_back"
    rollback_success = "rollback_success"
    rollback_message = "rollback_message"
    rollback_error = "rollback_error"

    copilot_analysis_start = "copilot_analysis_start"
    copilot_analyzing = "copilot_analyzing"
    copilot_analysis_success = "copilot_analysis_success"
    copilot_analysis_error = "copilot_analysis_error"

    copilot_template_prompt = "copilot_template_prompt"
    copilot_welcome_message = "copilot_welcome_message"


class JobCreateResponse(BaseModel):
    job_id: str = Field(...)
    status: JobStatus = JobStatus.received


class TrainingInput(BaseModel):
    """Input for training a model."""

    model_config = {"arbitrary_types_allowed": True}

    importer: TrainingDataImporter = Field(..., description="Training data importer")
    endpoints_file: Path = Field(..., description="Path to the endpoints file")
    config_file: Path = Field(..., description="Path to the config file")


class AgentStatus(str, Enum):
    """Status of the agent."""

    not_loaded = "not_loaded"
    ready = "ready"
    not_ready = "not_ready"
