from typing import Optional, Dict, Any, Union, List

from pydantic import field_validator, Field
from pydantic_core.core_schema import ValidationInfo

from phi.infra.base import InfraBase
from phi.app.context import ContainerContext
from phi.resource.base import ResourceBase
from phi.utils.log import logger


class AppBase(InfraBase):
    # -*- App Name (required)
    name: str

    # -*- Image Configuration
    # Image can be provided as a DockerImage object
    image: Optional[Any] = None
    # OR as image_name:image_tag str
    image_str: Optional[str] = None
    # OR as image_name and image_tag
    image_name: Optional[str] = None
    image_tag: Optional[str] = None
    # Entrypoint for the container
    entrypoint: Optional[Union[str, List[str]]] = None
    # Command for the container
    command: Optional[Union[str, List[str]]] = None

    # -*- Python Configuration
    # Install python dependencies using a requirements.txt file
    install_requirements: bool = False
    # Path to the requirements.txt file relative to the workspace_root
    requirements_file: str = "requirements.txt"
    # Set the PYTHONPATH env var
    set_python_path: bool = True
    # Manually provide the PYTHONPATH.
    # If None, PYTHONPATH is set to workspace_root
    python_path: Optional[str] = None
    # Add paths to the PYTHONPATH env var
    # If python_path is provided, this value is ignored
    add_python_paths: Optional[List[str]] = None

    # -*- App Ports
    # Open a container port if open_port=True
    open_port: bool = False
    # If open_port=True, port_number is used to set the
    # container_port if container_port is None and host_port if host_port is None
    port_number: int = 80
    # Port number on the Container to open
    # Preferred over port_number if both are set
    container_port: Optional[int] = Field(None, validate_default=True)
    # Port name for the opened port
    container_port_name: str = "http"
    # Port number on the Host to map to the Container port
    # Preferred over port_number if both are set
    host_port: Optional[int] = Field(None, validate_default=True)

    # -*- Extra Resources created "before" the App resources
    resources: Optional[List[ResourceBase]] = None

    #  -*- Other args
    print_env_on_load: bool = False

    # -*- App specific args. Not to be set by the user.
    # Container Environment that can be set by subclasses
    # which is used as a starting point for building the container_env
    # Any variables set in container_env will be overridden by values
    # in the env_vars dict or env_file
    container_env: Optional[Dict[str, Any]] = None
    # Variable used to cache the container context
    container_context: Optional[ContainerContext] = None

    # -*- Cached Data
    cached_resources: Optional[List[Any]] = None

    @field_validator("container_port", mode="before")
    def set_container_port(cls, v, info: ValidationInfo):
        port_number = info.data.get("port_number")
        if v is None and port_number is not None:
            v = port_number
        return v

    @field_validator("host_port", mode="before")
    def set_host_port(cls, v, info: ValidationInfo):
        port_number = info.data.get("port_number")
        if v is None and port_number is not None:
            v = port_number
        return v

    def get_app_name(self) -> str:
        return self.name

    def get_image_str(self) -> str:
        if self.image:
            return f"{self.image.name}:{self.image.tag}"
        elif self.image_str:
            return self.image_str
        elif self.image_name and self.image_tag:
            return f"{self.image_name}:{self.image_tag}"
        elif self.image_name:
            return f"{self.image_name}:latest"
        else:
            return ""

    def build_resources(self, build_context: Any) -> Optional[Any]:
        logger.debug(f"@build_resource_group not defined for {self.get_app_name()}")
        return None

    def get_dependencies(self) -> Optional[List[ResourceBase]]:
        return (
            [dep for dep in self.depends_on if isinstance(dep, ResourceBase)] if self.depends_on is not None else None
        )

    def add_app_properties_to_resources(self, resources: List[ResourceBase]) -> List[ResourceBase]:
        updated_resources = []
        app_properties = self.model_dump(exclude_defaults=True)
        app_group = self.get_group_name()
        app_output_dir = self.get_app_name()

        app_skip_create = app_properties.get("skip_create", None)
        app_skip_read = app_properties.get("skip_read", None)
        app_skip_update = app_properties.get("skip_update", None)
        app_skip_delete = app_properties.get("skip_delete", None)
        app_recreate_on_update = app_properties.get("recreate_on_update", None)
        app_use_cache = app_properties.get("use_cache", None)
        app_force = app_properties.get("force", None)
        app_debug_mode = app_properties.get("debug_mode", None)
        app_wait_for_create = app_properties.get("wait_for_create", None)
        app_wait_for_update = app_properties.get("wait_for_update", None)
        app_wait_for_delete = app_properties.get("wait_for_delete", None)
        app_save_output = app_properties.get("save_output", None)

        for resource in resources:
            resource_properties = resource.model_dump(exclude_defaults=True)
            resource_skip_create = resource_properties.get("skip_create", None)
            resource_skip_read = resource_properties.get("skip_read", None)
            resource_skip_update = resource_properties.get("skip_update", None)
            resource_skip_delete = resource_properties.get("skip_delete", None)
            resource_recreate_on_update = resource_properties.get("recreate_on_update", None)
            resource_use_cache = resource_properties.get("use_cache", None)
            resource_force = resource_properties.get("force", None)
            resource_debug_mode = resource_properties.get("debug_mode", None)
            resource_wait_for_create = resource_properties.get("wait_for_create", None)
            resource_wait_for_update = resource_properties.get("wait_for_update", None)
            resource_wait_for_delete = resource_properties.get("wait_for_delete", None)
            resource_save_output = resource_properties.get("save_output", None)

            # If skip_create on resource is not set, use app level skip_create (if set on app)
            if resource_skip_create is None and app_skip_create is not None:
                resource.skip_create = app_skip_create
            # If skip_read on resource is not set, use app level skip_read (if set on app)
            if resource_skip_read is None and app_skip_read is not None:
                resource.skip_read = app_skip_read
            # If skip_update on resource is not set, use app level skip_update (if set on app)
            if resource_skip_update is None and app_skip_update is not None:
                resource.skip_update = app_skip_update
            # If skip_delete on resource is not set, use app level skip_delete (if set on app)
            if resource_skip_delete is None and app_skip_delete is not None:
                resource.skip_delete = app_skip_delete
            # If recreate_on_update on resource is not set, use app level recreate_on_update (if set on app)
            if resource_recreate_on_update is None and app_recreate_on_update is not None:
                resource.recreate_on_update = app_recreate_on_update
            # If use_cache on resource is not set, use app level use_cache (if set on app)
            if resource_use_cache is None and app_use_cache is not None:
                resource.use_cache = app_use_cache
            # If force on resource is not set, use app level force (if set on app)
            if resource_force is None and app_force is not None:
                resource.force = app_force
            # If debug_mode on resource is not set, use app level debug_mode (if set on app)
            if resource_debug_mode is None and app_debug_mode is not None:
                resource.debug_mode = app_debug_mode
            # If wait_for_create on resource is not set, use app level wait_for_create (if set on app)
            if resource_wait_for_create is None and app_wait_for_create is not None:
                resource.wait_for_create = app_wait_for_create
            # If wait_for_update on resource is not set, use app level wait_for_update (if set on app)
            if resource_wait_for_update is None and app_wait_for_update is not None:
                resource.wait_for_update = app_wait_for_update
            # If wait_for_delete on resource is not set, use app level wait_for_delete (if set on app)
            if resource_wait_for_delete is None and app_wait_for_delete is not None:
                resource.wait_for_delete = app_wait_for_delete
            # If save_output on resource is not set, use app level save_output (if set on app)
            if resource_save_output is None and app_save_output is not None:
                resource.save_output = app_save_output
            # If workspace_settings on resource is not set, use app level workspace_settings (if set on app)
            if resource.workspace_settings is None and self.workspace_settings is not None:
                resource.set_workspace_settings(self.workspace_settings)
            # If group on resource is not set, use app level group (if set on app)
            if resource.group is None and app_group is not None:
                resource.group = app_group

            # Always set output_dir on resource to app level output_dir
            resource.output_dir = app_output_dir

            app_dependencies = self.get_dependencies()
            if app_dependencies is not None:
                if resource.depends_on is None:
                    resource.depends_on = app_dependencies
                else:
                    resource.depends_on.extend(app_dependencies)

            updated_resources.append(resource)
        return updated_resources

    def get_resources(self, build_context: Any) -> List[ResourceBase]:
        if self.cached_resources is not None and len(self.cached_resources) > 0:
            return self.cached_resources

        base_resources = self.resources or []
        app_resources = self.build_resources(build_context)
        if app_resources is not None:
            base_resources.extend(app_resources)

        self.cached_resources = self.add_app_properties_to_resources(base_resources)
        # logger.debug(f"Resources: {self.cached_resources}")
        return self.cached_resources

    def matches_filters(self, group_filter: Optional[str] = None) -> bool:
        if group_filter is not None:
            group_name = self.get_group_name()
            logger.debug(f"{self.get_app_name()}: Checking {group_filter} in {group_name}")
            if group_name is None or group_filter not in group_name:
                return False
        return True

    def should_create(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_create:
            return False
        return self.matches_filters(group_filter)

    def should_delete(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_delete:
            return False
        return self.matches_filters(group_filter)

    def should_update(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_update:
            return False
        return self.matches_filters(group_filter)
