from datetime import datetime
from pathlib import Path
from typing import Literal

from pydantic import Field, HttpUrl

from .base import Base

APPSERVER_TAG_PREFIX = "appserver-"


def version_to_image_tag(version: str) -> str:
    """Convert a llama_deploy_version like '0.4.2' to an image tag like 'appserver-0.4.2'."""
    return f"{APPSERVER_TAG_PREFIX}{version}"


def image_tag_to_version(tag: str) -> str | None:
    """Extract version from image tag, or None if tag doesn't follow the appserver-X.Y.Z convention."""
    if tag.startswith(APPSERVER_TAG_PREFIX):
        return tag.removeprefix(APPSERVER_TAG_PREFIX)
    return None


# K8s CRD phase values
LlamaDeploymentPhase = Literal[
    "Pending",  # Waiting for deployment resources to be ready (pods starting up)
    "Running",  # Deployment is healthy and serving traffic
    "Failed",  # Complete deployment failure - no pods available
    "RollingOut",  # Rolling update in progress - new pods being created while old ones still serve traffic
    "RolloutFailed",  # New deployment failed but old pods are still available and serving traffic
    "Suspended",  # Deployment is suspended (scaled to 0 replicas)
    "Building",  # Build Job is in progress
    "BuildFailed",  # Build Job failed
]


class DeploymentEvent(Base):
    message: str | None = None
    reason: str | None = None
    type: str | None = None
    first_timestamp: datetime | None = None
    last_timestamp: datetime | None = None
    count: int | None = None


class DeploymentResponse(Base):
    id: str
    name: str
    repo_url: str
    deployment_file_path: str
    git_ref: str | None = None
    git_sha: str | None = None
    has_personal_access_token: bool = False
    project_id: str
    secret_names: list[str] | None = None
    apiserver_url: HttpUrl | None
    status: LlamaDeploymentPhase
    warning: str | None = None
    events: list[DeploymentEvent] | None = None
    # version selector for appserver image
    llama_deploy_version: str | None = None
    suspended: bool = False


class DeploymentsListResponse(Base):
    deployments: list[DeploymentResponse]


class DeploymentCreate(Base):
    name: str
    repo_url: str
    deployment_file_path: str | None = None
    git_ref: str | None = None
    personal_access_token: str | None = None
    secrets: dict[str, str] | None = None
    # optional version selector; if omitted, server may set based on client version
    llama_deploy_version: str | None = None


class LlamaDeploymentMetadata(Base):
    name: str
    namespace: str
    uid: str | None = None
    resourceVersion: str | None = None
    creationTimestamp: datetime | None = None
    annotations: dict[str, str] | None = None
    labels: dict[str, str] | None = None


class LlamaDeploymentSpec(Base):
    """
    LlamaDeployment spec fields as defined in the Kubernetes CRD.

    Maps to the spec section of the LlamaDeployment custom resource.
    Field names match exactly with the K8s CRD for direct conversion.
    """

    projectId: str
    repoUrl: str
    deploymentFilePath: str = "."
    gitRef: str | None = None
    gitSha: str | None = None
    name: str
    secretName: str | None = None
    # when true, the deployment will prebuild the UI assets and serve them from a static file server
    staticAssetsPath: str | None = None
    # explicit imageTag (operator will use this if provided)
    imageTag: str | None = None
    suspended: bool = False
    # monotonically increasing counter to force a new build (retry failed builds)
    buildGeneration: int = 0


class LlamaDeploymentStatus(Base):
    """
    LlamaDeployment status fields as defined in the Kubernetes CRD.

    Maps to the status section of the LlamaDeployment custom resource.
    """

    phase: LlamaDeploymentPhase | None = None
    message: str | None = None
    lastUpdated: datetime | None = None
    authToken: str | None = None
    # Historical list of released versions from the CRD (camelCase fields)
    releaseHistory: list["ReleaseHistoryEntry"] | None = None


class LlamaDeploymentCRD(Base):
    metadata: LlamaDeploymentMetadata
    spec: LlamaDeploymentSpec
    status: LlamaDeploymentStatus = Field(default_factory=LlamaDeploymentStatus)


class DeploymentUpdate(Base):
    """
    Patch-style update model for deployments.

    Fields not included in the request will remain unchanged.
    Fields explicitly set to None will clear/delete the field value.

    For secrets: provide a dict where string values add/update secrets
    and null values remove secrets.
    """

    repo_url: str | None = None
    deployment_file_path: str | None = None
    git_ref: str | None = None
    git_sha: str | None = None
    personal_access_token: str | None = None
    secrets: dict[str, str | None] | None = None
    static_assets_path: Path | None = None
    # allow updating version selector
    llama_deploy_version: str | None = None
    # explicit image tag (takes precedence over llama_deploy_version)
    image_tag: str | None = None
    # bump the appserver image to the cluster's current default version
    bump_to_latest_appserver: bool = False
    suspended: bool | None = None
    # increment to force a rebuild (e.g. retry after transient network failure)
    rebuild: bool = False

    def has_git_fields(self) -> bool:
        """Return True if any git-affecting fields are set."""
        return any(
            [
                self.repo_url is not None,
                self.deployment_file_path is not None,
                self.git_ref is not None,
                self.personal_access_token is not None,
            ]
        )

    def _has_substantive_fields(self) -> bool:
        """Return True if update contains fields that affect the running deployment."""
        return any(
            [
                self.repo_url is not None,
                self.deployment_file_path is not None,
                self.git_ref is not None,
                self.personal_access_token is not None,
                self.secrets is not None,
                self.image_tag is not None,
                self.llama_deploy_version is not None,
                self.bump_to_latest_appserver,
            ]
        )


class DeploymentUpdateResult(Base):
    """
    Result of applying a DeploymentUpdate to a LlamaDeploymentSpec.

    Contains the updated spec and lists of secret changes to apply.
    """

    updated_spec: LlamaDeploymentSpec
    secret_adds: dict[str, str]
    secret_removes: list[str]


def apply_deployment_update(
    update: DeploymentUpdate,
    existing_spec: LlamaDeploymentSpec,
) -> DeploymentUpdateResult:
    """
    Apply a DeploymentUpdate to an existing LlamaDeploymentSpec.

    Returns the updated spec and lists of secret changes.

    Args:
        update: The update to apply (snake_case fields from API)
        existing_spec: The current LlamaDeploymentSpec (camelCase fields)
        git_sha: The resolved git SHA to set

    Returns:
        DeploymentUpdateResult with updated spec and secret changes
    """
    # Start with a copy of the existing spec
    updated_spec = existing_spec.model_copy()

    # Apply direct field updates (only if not None)
    # Convert from snake_case API fields to camelCase spec fields
    if update.repo_url is not None:
        updated_spec.repoUrl = update.repo_url

    if update.deployment_file_path is not None:
        updated_spec.deploymentFilePath = update.deployment_file_path

    if update.git_ref is not None:
        updated_spec.gitRef = update.git_ref

    # Update gitSha if provided
    if update.git_sha is not None:
        updated_spec.gitSha = None if update.git_sha == "" else update.git_sha

    if update.static_assets_path is not None:
        updated_spec.staticAssetsPath = str(update.static_assets_path)

    # Track secret changes
    secret_adds: dict[str, str] = {}
    secret_removes: list[str] = []

    # Handle personal access token (stored as GITHUB_PAT secret)
    if update.personal_access_token is not None:
        if update.personal_access_token == "":
            # Empty string means remove the PAT
            secret_removes.append("GITHUB_PAT")
        else:
            # Non-empty string means add/update the PAT
            secret_adds["GITHUB_PAT"] = update.personal_access_token

    # Handle explicit secret updates
    secrets = update.secrets
    if secrets is not None:
        for key, value in secrets.items():
            if value is None:
                # None means remove this secret
                secret_removes.append(key)
            else:
                # String value means add/update this secret
                secret_adds[key] = value

    if update.suspended is not None:
        updated_spec.suspended = update.suspended

    # Auto-resume: if the deployment was suspended and this update contains
    # substantive fields (but doesn't explicitly set suspended), resume it.
    if (
        update.suspended is None
        and existing_spec.suspended
        and update._has_substantive_fields()
    ):
        updated_spec.suspended = False

    # Handle image tag / version selector (image_tag takes precedence)
    if update.image_tag is not None:
        updated_spec.imageTag = update.image_tag
    elif update.llama_deploy_version is not None:
        updated_spec.imageTag = version_to_image_tag(update.llama_deploy_version)

    # Bump buildGeneration to force a rebuild (e.g. retry after transient failure)
    if update.rebuild:
        updated_spec.buildGeneration = existing_spec.buildGeneration + 1

    return DeploymentUpdateResult(
        updated_spec=updated_spec,
        secret_adds=secret_adds,
        secret_removes=secret_removes,
    )


class LogEvent(Base):
    pod: str
    container: str
    text: str
    timestamp: datetime


# ===== Release history models =====


class ReleaseHistoryEntry(Base):
    """
    Mirrors the CRD status.releaseHistory entry with camelCase keys.
    """

    gitSha: str
    imageTag: str | None = None
    releasedAt: datetime


class ReleaseHistoryItem(Base):
    """
    API-exposed release history item with snake_case keys for clients.
    """

    git_sha: str
    image_tag: str | None = None
    released_at: datetime


class DeploymentHistoryResponse(Base):
    deployment_id: str
    history: list[ReleaseHistoryItem]


class RollbackRequest(Base):
    git_sha: str
    image_tag: str | None = None
