from __future__ import annotations

import datetime
import time
from enum import Enum
from typing import Any, Dict, List, Optional, Union

import jwt
from pydantic import BaseModel, Extra, Field, constr, validator

# TODO: switch to Enums for str literals
# TODO: Need a better approach to keep fields in sync with server
#       most fields should have a default in case server adds/removes a field
# TODO: Implement NotImplementedError sections
UNDEFINED_STRING = "<Undefined>"


class Base(BaseModel):
    class Config:
        validate_assignment = True
        use_enum_values = True
        extra = Extra.allow

    def __repr_args__(self):
        return [
            (key, value)
            for key, value in self.__dict__.items()
            if key in self.__fields__
            and self.__fields__[key].field_info.extra.get("repr", True)
        ]


class Entity(Base):
    createdAt: datetime.datetime = Field(repr=False)
    updatedAt: datetime.datetime = Field(repr=False)
    createdBy: str = Field(repr=False)


class Cluster(Entity):
    id: str = Field(repr=False)
    name: str
    fqn: str
    region: str = Field(repr=False)
    isTenantDefault: bool = False
    isAvailable: bool = False

    def list_row_data(self) -> Dict[str, Any]:
        return {
            "ClusterName": self.name,
            "Region": self.region,
            "CreatedAt": self.createdAt,
        }

    def get_data(self) -> Dict[str, Any]:
        return {
            "ClusterName": self.name,
            "Region": self.region,
            "CreatedAt": self.createdAt,
            "UpdatedAt": self.updatedAt,
            "CreatedBy": self.createdBy,
        }

    @property
    def workspaces(self) -> List["Workspace"]:
        raise NotImplementedError


class Workspace(Entity):
    id: str = Field(repr=False)
    name: str
    fqn: str
    clusterId: str = Field(repr=False)

    def list_row_data(self) -> Dict[str, Any]:
        return {
            "WorkspaceName": self.name,
            "WorkspaceFQN": self.fqn,
            "ClusterName": self.clusterId,
            "CreatedAt": self.createdAt,
        }

    def get_data(self) -> Dict[str, Any]:
        return {
            "WorkspaceName": self.name,
            "WorkspaceFQN": self.fqn,
            "ClusterName": self.clusterId,
            "CreatedAt": self.createdAt,
            "UpdatedAt": self.updatedAt,
            "CreatedBy": self.createdBy,
        }

    @property
    def cluster(self) -> Cluster:
        raise NotImplementedError


class WorkspaceResources(BaseModel):
    cpu_limit: Optional[float]
    memory_limit: Optional[int]
    ephemeral_storage_limit: Optional[int]


class PortMetadata(BaseModel):
    port: int
    # TODO: done because of a bug, needs to reverted to be mandatory
    host: Optional[str]


class DeploymentTransitionStatus(str, Enum):
    INITIALIZED: str = "INITIALIZED"
    BUILDING: str = "BUILDING"
    DEPLOYING: str = "DEPLOYING"
    BUILD_SUCCESS: str = "BUILD_SUCCESS"
    DEPLOY_SUCCESS: str = "DEPLOY_SUCCESS"
    DEPLOY_FAILED: str = "DEPLOY_FAILED"
    BUILD_FAILED: str = "BUILD_FAILED"
    CANCELLED: str = "CANCELLED"
    FAILED: str = "FAILED"
    REUSING_EXISTING_BUILD: str = "REUSING_EXISTING_BUILD"
    _: str = ""

    @classmethod
    def is_failure_state(cls, state: DeploymentTransitionStatus) -> bool:
        return state in (cls.DEPLOY_FAILED, cls.BUILD_FAILED, cls.FAILED, cls.CANCELLED)


class DeploymentState(BaseModel):
    isTerminalState: bool


class DeploymentStatus(BaseModel):
    state: DeploymentState
    status: DeploymentTransitionStatus
    transition: Optional[DeploymentTransitionStatus]


class DeploymentManifest(Base):
    name: Optional[str] = Field(default=UNDEFINED_STRING)
    type: str


class Deployment(Entity):
    id: str = Field(repr=False)
    fqn: str
    version: int
    currentStatusId: str = Field(repr=False)
    applicationId: str = Field(repr=False)
    manifest: DeploymentManifest = Field(repr=False)
    failureReason: Optional[str]
    deploymentStatuses: Optional[List[DeploymentStatus]]
    # TODO: Dict -> pydantic model if required
    # application: Dict[str, Any]
    # TODO: Dict -> pydantic model if required
    # workspace: Dict[str, Any]
    # baseDomainURL: str
    # builds: List[BuildResponse]
    metadata: Optional[
        Union[Dict[str, Any], List[Dict[str, Any]]]
    ]  # TODO (chiragjn): revisit the type of this field
    currentStatus: Optional[DeploymentStatus]  # Server was not returning CurrentStatus

    def list_row_data(self) -> Dict[str, Any]:
        return {
            "DeploymentFQN": self.fqn,
            "ApplicationName": self.manifest.name,
            "ApplicationType": self.manifest.type,
            "Version": self.version,
            "CreatedAt": self.createdAt,
        }

    def get_data(self) -> Dict[str, Any]:
        return {
            "DeploymentFQN": self.fqn,
            "ApplicationName": self.manifest.name,
            "ApplicationType": self.manifest.type,
            "Version": self.version,
            "WorkspaceName": self.fqn.split(":")[1],  # TODO: change hardcoding
            "ClusterName": self.fqn.split(":")[0],  # TODO: change hardcoding
            "CreatedAt": self.createdAt,
            "UpdatedAt": self.updatedAt,
            "CreatedBy": self.createdBy,
        }

    class Config:
        extra = Extra.allow


class ApplicationWorkspace(Base):
    name: str
    fqn: str
    clusterId: str


class Application(Entity):
    id: str = Field(repr=False)
    name: str
    fqn: str
    tenantName: str
    workspaceId: str = Field(repr=False)
    lastVersion: int
    activeVersion: int
    workspace: ApplicationWorkspace
    deployment: Deployment
    activeDeploymentId: Optional[str] = Field(repr=False)
    lastDeploymentId: str = Field(repr=False)

    def list_row_data(self) -> Dict[str, Any]:
        return {
            "ApplicationName": self.name,
            "ApplicationFQN": self.fqn,
            "ActiveVersion": self.activeVersion,
            "LastVersion": self.lastVersion,
            "WorkspaceName": self.workspace.name,
            "CreatedAt": self.createdAt,
        }

    def get_data(self) -> Dict[str, Any]:
        return {
            "ApplicationName": self.name,
            "ApplicationFQN": self.fqn,
            "ActiveVersion": self.activeVersion,
            "LastVersion": self.lastVersion,
            "WorkspaceFQN": self.workspace.fqn,
            "ClusterFQN": self.workspace.clusterId,
            "CreatedAt": self.createdAt,
            "UpdatedAt": self.updatedAt,
            "CreatedBy": self.createdBy,
        }


class UserType(Enum):
    user = "user"
    serviceaccount = "serviceaccount"


class UserInfo(BaseModel):
    user_id: constr(min_length=1)
    user_type: UserType = UserType.user
    email: Optional[str] = None
    tenant_name: constr(min_length=1) = Field(alias="tenantName")

    class Config:
        allow_population_by_field_name = True
        allow_mutation = False


class TenantInfo(BaseModel):
    tenant_name: constr(min_length=1) = Field(alias="tenantName")
    auth_server_url: str

    class Config:
        allow_population_by_field_name = True
        allow_mutation = False


class Token(BaseModel):
    access_token: constr(min_length=1) = Field(alias="accessToken", repr=False)
    refresh_token: Optional[constr(min_length=1)] = Field(
        alias="refreshToken", repr=False
    )
    decoded_value: Optional[Dict] = Field(exclude=True, repr=False)

    class Config:
        allow_population_by_field_name = True
        allow_mutation = False

    @validator("decoded_value", always=True, pre=True)
    def _decode_jwt(cls, v, values, **kwargs):
        access_token = values["access_token"]
        return jwt.decode(
            access_token,
            options={
                "verify_signature": False,
                "verify_aud": False,
                "verify_exp": False,
            },
        )

    @property
    def tenant_name(self) -> str:
        return self.decoded_value["tenantName"]

    def is_going_to_be_expired(self, buffer_in_seconds: int = 120) -> bool:
        exp = int(self.decoded_value["exp"])
        return (exp - time.time()) < buffer_in_seconds

    def to_user_info(self) -> UserInfo:
        return UserInfo(
            user_id=self.decoded_value["username"],
            email=self.decoded_value["email"]
            if "email" in self.decoded_value
            else None,
            user_type=UserType(self.decoded_value.get("userType", UserType.user.value)),
            tenant_name=self.tenant_name,
        )


class CredentialsFileContent(BaseModel):
    access_token: constr(min_length=1) = Field(repr=False)
    refresh_token: Optional[constr(min_length=1)] = Field(repr=False)
    host: constr(min_length=1)

    class Config:
        allow_mutation = False

    def to_token(self) -> Token:
        return Token(access_token=self.access_token, refresh_token=self.refresh_token)


class DeviceCode(BaseModel):
    user_code: str = Field(alias="userCode")
    device_code: str = Field(alias="deviceCode")

    class Config:
        allow_population_by_field_name = True
        allow_mutation = False

    def get_user_clickable_url(self, auth_host: str) -> str:
        return f"{auth_host}/authorize/device?userCode={self.user_code}"


class TriggerJobResult(Base):
    message: str = Field(default="Unknown")


class DockerRegistryCredentials(Base):
    fqn: str
    registryUrl: str
    username: str
    password: str


class CreateDockerRepositoryResponse(Base):
    repoName: str
