from typing import List, Optional, Union, Tuple

from phi.app.group import AppGroup
from phi.resource.group import ResourceGroup
from phi.docker.app.base import DockerApp
from phi.docker.app.context import DockerBuildContext
from phi.docker.api_client import DockerApiClient
from phi.docker.resource.base import DockerResource
from phi.infra.resources import InfraResources
from phi.workspace.settings import WorkspaceSettings
from phi.utils.log import logger


class DockerResources(InfraResources):
    env: str = "dev"
    network: str = "phi"
    # URL for the Docker server. For example, unix:///var/run/docker.sock or tcp://127.0.0.1:1234
    base_url: Optional[str] = None

    apps: Optional[List[Union[DockerApp, AppGroup]]] = None
    resources: Optional[List[Union[DockerResource, ResourceGroup]]] = None

    # -*- Cached Data
    _api_client: Optional[DockerApiClient] = None

    @property
    def docker_client(self) -> DockerApiClient:
        if self._api_client is None:
            self._api_client = DockerApiClient(base_url=self.base_url)
        return self._api_client

    def create_resources(
        self,
        group_filter: Optional[str] = None,
        name_filter: Optional[str] = None,
        type_filter: Optional[str] = None,
        dry_run: Optional[bool] = False,
        auto_confirm: Optional[bool] = False,
        force: Optional[bool] = None,
    ) -> Tuple[int, int]:
        from phi.cli.console import print_info, print_heading, confirm_yes_no
        from phi.docker.resource.types import DockerContainer, DockerResourceInstallOrder

        logger.debug("-*- Creating DockerResources")
        # Build a list of DockerResources to create
        resources_to_create: List[DockerResource] = []
        if self.resources is not None:
            for r in self.resources:
                if isinstance(r, ResourceGroup):
                    resources_from_resource_group = r.get_resources()
                    if len(resources_from_resource_group) > 0:
                        for resource_from_resource_group in resources_from_resource_group:
                            if isinstance(resource_from_resource_group, DockerResource):
                                resource_from_resource_group.set_workspace_settings(
                                    workspace_settings=self.workspace_settings
                                )
                                if resource_from_resource_group.group is None and self.name is not None:
                                    resource_from_resource_group.group = self.name
                                if resource_from_resource_group.should_create(
                                    group_filter=group_filter,
                                    name_filter=name_filter,
                                    type_filter=type_filter,
                                ):
                                    resources_to_create.append(resource_from_resource_group)
                elif isinstance(r, DockerResource):
                    r.set_workspace_settings(workspace_settings=self.workspace_settings)
                    if r.group is None and self.name is not None:
                        r.group = self.name
                    if r.should_create(
                        group_filter=group_filter,
                        name_filter=name_filter,
                        type_filter=type_filter,
                    ):
                        r.set_workspace_settings(workspace_settings=self.workspace_settings)
                        resources_to_create.append(r)

        # Build a list of DockerApps to create
        apps_to_create: List[DockerApp] = []
        if self.apps is not None:
            for app in self.apps:
                if isinstance(app, AppGroup):
                    apps_from_app_group = app.get_apps()
                    if len(apps_from_app_group) > 0:
                        for app_from_app_group in apps_from_app_group:
                            if isinstance(app_from_app_group, DockerApp):
                                if app_from_app_group.group is None and self.name is not None:
                                    app_from_app_group.group = self.name
                                if app_from_app_group.should_create(group_filter=group_filter):
                                    apps_to_create.append(app_from_app_group)
                elif isinstance(app, DockerApp):
                    if app.group is None and self.name is not None:
                        app.group = self.name
                    if app.should_create(group_filter=group_filter):
                        apps_to_create.append(app)

        # Get the list of DockerResources from the DockerApps
        if len(apps_to_create) > 0:
            logger.debug(f"Found {len(apps_to_create)} apps to create")
            for app in apps_to_create:
                app.set_workspace_settings(workspace_settings=self.workspace_settings)
                app_resources = app.get_resources(build_context=DockerBuildContext(network=self.network))
                if len(app_resources) > 0:
                    # If the app has dependencies, add the resources from the
                    # dependencies first to the list of resources to create
                    if app.depends_on is not None:
                        for dep in app.depends_on:
                            if isinstance(dep, DockerApp):
                                dep.set_workspace_settings(workspace_settings=self.workspace_settings)
                                dep_resources = dep.get_resources(
                                    build_context=DockerBuildContext(network=self.network)
                                )
                                if len(dep_resources) > 0:
                                    for dep_resource in dep_resources:
                                        if isinstance(dep_resource, DockerResource):
                                            resources_to_create.append(dep_resource)
                    # Add the resources from the app to the list of resources to create
                    for app_resource in app_resources:
                        if isinstance(app_resource, DockerResource) and app_resource.should_create(
                            group_filter=group_filter, name_filter=name_filter, type_filter=type_filter
                        ):
                            resources_to_create.append(app_resource)

        # Sort the DockerResources in install order
        resources_to_create.sort(key=lambda x: DockerResourceInstallOrder.get(x.__class__.__name__, 5000))

        # Deduplicate DockerResources
        deduped_resources_to_create: List[DockerResource] = []
        for r in resources_to_create:
            if r not in deduped_resources_to_create:
                deduped_resources_to_create.append(r)

        # Implement dependency sorting
        final_docker_resources: List[DockerResource] = []
        logger.debug("-*- Building DockerResources dependency graph")
        for docker_resource in deduped_resources_to_create:
            # Logic to follow if resource has dependencies
            if docker_resource.depends_on is not None:
                # Add the dependencies before the resource itself
                for dep in docker_resource.depends_on:
                    if isinstance(dep, DockerResource):
                        if dep not in final_docker_resources:
                            logger.debug(f"-*- Adding {dep.name}, dependency of {docker_resource.name}")
                            final_docker_resources.append(dep)

                # Add the resource to be created after its dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)
            else:
                # Add the resource to be created if it has no dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)

        # Track the total number of DockerResources to create for validation
        num_resources_to_create: int = len(final_docker_resources)
        num_resources_created: int = 0
        if num_resources_to_create == 0:
            return 0, 0

        if dry_run:
            print_heading("--**- Docker resources to create:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_create} resources")
            return 0, 0

        # Validate resources to be created
        if not auto_confirm:
            print_heading("\n--**-- Confirm resources to create:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_create} resources")
            confirm = confirm_yes_no("\nConfirm deploy")
            if not confirm:
                print_info("-*-")
                print_info("-*- Skipping create")
                print_info("-*-")
                return 0, 0

        for resource in final_docker_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            if isinstance(resource, DockerContainer):
                if resource.network is None and self.network is not None:
                    resource.network = self.network
            # logger.debug(resource)
            try:
                _resource_created = resource.create(docker_client=self.docker_client)
                if _resource_created:
                    num_resources_created += 1
                else:
                    if self.workspace_settings is not None and not self.workspace_settings.continue_on_create_failure:
                        return num_resources_created, num_resources_to_create
            except Exception as e:
                logger.error(f"Failed to create {resource.get_resource_type()}: {resource.get_resource_name()}")
                logger.error(e)
                logger.error("Please fix and try again...")

        print_heading(f"\n--**-- Resources created: {num_resources_created}/{num_resources_to_create}")
        if num_resources_to_create != num_resources_created:
            logger.error(
                f"Resources created: {num_resources_created} do not match resources required: {num_resources_to_create}"
            )  # noqa: E501
        return num_resources_created, num_resources_to_create

    def delete_resources(
        self,
        group_filter: Optional[str] = None,
        name_filter: Optional[str] = None,
        type_filter: Optional[str] = None,
        dry_run: Optional[bool] = False,
        auto_confirm: Optional[bool] = False,
        force: Optional[bool] = None,
    ) -> Tuple[int, int]:
        from phi.cli.console import print_info, print_heading, confirm_yes_no
        from phi.docker.resource.types import DockerContainer, DockerResourceInstallOrder

        logger.debug("-*- Deleting DockerResources")
        # Build a list of DockerResources to delete
        resources_to_delete: List[DockerResource] = []
        if self.resources is not None:
            for r in self.resources:
                if isinstance(r, ResourceGroup):
                    resources_from_resource_group = r.get_resources()
                    if len(resources_from_resource_group) > 0:
                        for resource_from_resource_group in resources_from_resource_group:
                            if isinstance(resource_from_resource_group, DockerResource):
                                resource_from_resource_group.set_workspace_settings(
                                    workspace_settings=self.workspace_settings
                                )
                                if resource_from_resource_group.group is None and self.name is not None:
                                    resource_from_resource_group.group = self.name
                                if resource_from_resource_group.should_delete(
                                    group_filter=group_filter,
                                    name_filter=name_filter,
                                    type_filter=type_filter,
                                ):
                                    resources_to_delete.append(resource_from_resource_group)
                elif isinstance(r, DockerResource):
                    r.set_workspace_settings(workspace_settings=self.workspace_settings)
                    if r.group is None and self.name is not None:
                        r.group = self.name
                    if r.should_delete(
                        group_filter=group_filter,
                        name_filter=name_filter,
                        type_filter=type_filter,
                    ):
                        r.set_workspace_settings(workspace_settings=self.workspace_settings)
                        resources_to_delete.append(r)

        # Build a list of DockerApps to delete
        apps_to_delete: List[DockerApp] = []
        if self.apps is not None:
            for app in self.apps:
                if isinstance(app, AppGroup):
                    apps_from_app_group = app.get_apps()
                    if len(apps_from_app_group) > 0:
                        for app_from_app_group in apps_from_app_group:
                            if isinstance(app_from_app_group, DockerApp):
                                if app_from_app_group.group is None and self.name is not None:
                                    app_from_app_group.group = self.name
                                if app_from_app_group.should_delete(group_filter=group_filter):
                                    apps_to_delete.append(app_from_app_group)
                elif isinstance(app, DockerApp):
                    if app.group is None and self.name is not None:
                        app.group = self.name
                    if app.should_delete(group_filter=group_filter):
                        apps_to_delete.append(app)

        # Get the list of DockerResources from the DockerApps
        if len(apps_to_delete) > 0:
            logger.debug(f"Found {len(apps_to_delete)} apps to delete")
            for app in apps_to_delete:
                app.set_workspace_settings(workspace_settings=self.workspace_settings)
                app_resources = app.get_resources(build_context=DockerBuildContext(network=self.network))
                if len(app_resources) > 0:
                    # Add the resources from the app to the list of resources to delete
                    for app_resource in app_resources:
                        if isinstance(app_resource, DockerResource) and app_resource.should_delete(
                            group_filter=group_filter, name_filter=name_filter, type_filter=type_filter
                        ):
                            resources_to_delete.append(app_resource)
                    # # If the app has dependencies, add the resources from the
                    # # dependencies to the list of resources to delete
                    # if app.depends_on is not None:
                    #     for dep in app.depends_on:
                    #         if isinstance(dep, DockerApp):
                    #             dep.set_workspace_settings(workspace_settings=self.workspace_settings)
                    #             dep_resources = dep.get_resources(
                    #                 build_context=DockerBuildContext(network=self.network)
                    #             )
                    #             if len(dep_resources) > 0:
                    #                 for dep_resource in dep_resources:
                    #                     if isinstance(dep_resource, DockerResource):
                    #                         resources_to_delete.append(dep_resource)

        # Sort the DockerResources in install order
        resources_to_delete.sort(key=lambda x: DockerResourceInstallOrder.get(x.__class__.__name__, 5000), reverse=True)

        # Deduplicate DockerResources
        deduped_resources_to_delete: List[DockerResource] = []
        for r in resources_to_delete:
            if r not in deduped_resources_to_delete:
                deduped_resources_to_delete.append(r)

        # Implement dependency sorting
        final_docker_resources: List[DockerResource] = []
        logger.debug("-*- Building DockerResources dependency graph")
        for docker_resource in deduped_resources_to_delete:
            # Logic to follow if resource has dependencies
            if docker_resource.depends_on is not None:
                # 1. Reverse the order of dependencies
                docker_resource.depends_on.reverse()

                # 2. Remove the dependencies if they are already added to the final_docker_resources
                for dep in docker_resource.depends_on:
                    if dep in final_docker_resources:
                        logger.debug(f"-*- Removing {dep.name}, dependency of {docker_resource.name}")
                        final_docker_resources.remove(dep)

                # 3. Add the resource to be deleted before its dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)

                # 4. Add the dependencies back in reverse order
                for dep in docker_resource.depends_on:
                    if isinstance(dep, DockerResource):
                        if dep not in final_docker_resources:
                            logger.debug(f"-*- Adding {dep.name}, dependency of {docker_resource.name}")
                            final_docker_resources.append(dep)
            else:
                # Add the resource to be deleted if it has no dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)

        # Track the total number of DockerResources to delete for validation
        num_resources_to_delete: int = len(final_docker_resources)
        num_resources_deleted: int = 0
        if num_resources_to_delete == 0:
            return 0, 0

        if dry_run:
            print_heading("--**- Docker resources to delete:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_delete} resources")
            return 0, 0

        # Validate resources to be deleted
        if not auto_confirm:
            print_heading("\n--**-- Confirm resources to delete:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_delete} resources")
            confirm = confirm_yes_no("\nConfirm delete")
            if not confirm:
                print_info("-*-")
                print_info("-*- Skipping delete")
                print_info("-*-")
                return 0, 0

        for resource in final_docker_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            if isinstance(resource, DockerContainer):
                if resource.network is None and self.network is not None:
                    resource.network = self.network
            # logger.debug(resource)
            try:
                _resource_deleted = resource.delete(docker_client=self.docker_client)
                if _resource_deleted:
                    num_resources_deleted += 1
                else:
                    if self.workspace_settings is not None and not self.workspace_settings.continue_on_delete_failure:
                        return num_resources_deleted, num_resources_to_delete
            except Exception as e:
                logger.error(f"Failed to delete {resource.get_resource_type()}: {resource.get_resource_name()}")
                logger.error(e)
                logger.error("Please fix and try again...")

        print_heading(f"\n--**-- Resources deleted: {num_resources_deleted}/{num_resources_to_delete}")
        if num_resources_to_delete != num_resources_deleted:
            logger.error(
                f"Resources deleted: {num_resources_deleted} do not match resources required: {num_resources_to_delete}"
            )  # noqa: E501
        return num_resources_deleted, num_resources_to_delete

    def update_resources(
        self,
        group_filter: Optional[str] = None,
        name_filter: Optional[str] = None,
        type_filter: Optional[str] = None,
        dry_run: Optional[bool] = False,
        auto_confirm: Optional[bool] = False,
        force: Optional[bool] = None,
    ) -> Tuple[int, int]:
        from phi.cli.console import print_info, print_heading, confirm_yes_no
        from phi.docker.resource.types import DockerContainer, DockerResourceInstallOrder

        logger.debug("-*- Updating DockerResources")

        # Build a list of DockerResources to update
        resources_to_update: List[DockerResource] = []
        if self.resources is not None:
            for r in self.resources:
                if isinstance(r, ResourceGroup):
                    resources_from_resource_group = r.get_resources()
                    if len(resources_from_resource_group) > 0:
                        for resource_from_resource_group in resources_from_resource_group:
                            if isinstance(resource_from_resource_group, DockerResource):
                                resource_from_resource_group.set_workspace_settings(
                                    workspace_settings=self.workspace_settings
                                )
                                if resource_from_resource_group.group is None and self.name is not None:
                                    resource_from_resource_group.group = self.name
                                if resource_from_resource_group.should_update(
                                    group_filter=group_filter,
                                    name_filter=name_filter,
                                    type_filter=type_filter,
                                ):
                                    resources_to_update.append(resource_from_resource_group)
                elif isinstance(r, DockerResource):
                    r.set_workspace_settings(workspace_settings=self.workspace_settings)
                    if r.group is None and self.name is not None:
                        r.group = self.name
                    if r.should_update(
                        group_filter=group_filter,
                        name_filter=name_filter,
                        type_filter=type_filter,
                    ):
                        r.set_workspace_settings(workspace_settings=self.workspace_settings)
                        resources_to_update.append(r)

        # Build a list of DockerApps to update
        apps_to_update: List[DockerApp] = []
        if self.apps is not None:
            for app in self.apps:
                if isinstance(app, AppGroup):
                    apps_from_app_group = app.get_apps()
                    if len(apps_from_app_group) > 0:
                        for app_from_app_group in apps_from_app_group:
                            if isinstance(app_from_app_group, DockerApp):
                                if app_from_app_group.group is None and self.name is not None:
                                    app_from_app_group.group = self.name
                                if app_from_app_group.should_update(group_filter=group_filter):
                                    apps_to_update.append(app_from_app_group)
                elif isinstance(app, DockerApp):
                    if app.group is None and self.name is not None:
                        app.group = self.name
                    if app.should_update(group_filter=group_filter):
                        apps_to_update.append(app)

        # Get the list of DockerResources from the DockerApps
        if len(apps_to_update) > 0:
            logger.debug(f"Found {len(apps_to_update)} apps to update")
            for app in apps_to_update:
                app.set_workspace_settings(workspace_settings=self.workspace_settings)
                app_resources = app.get_resources(build_context=DockerBuildContext(network=self.network))
                if len(app_resources) > 0:
                    # # If the app has dependencies, add the resources from the
                    # # dependencies first to the list of resources to update
                    # if app.depends_on is not None:
                    #     for dep in app.depends_on:
                    #         if isinstance(dep, DockerApp):
                    #             dep.set_workspace_settings(workspace_settings=self.workspace_settings)
                    #             dep_resources = dep.get_resources(
                    #                 build_context=DockerBuildContext(network=self.network)
                    #             )
                    #             if len(dep_resources) > 0:
                    #                 for dep_resource in dep_resources:
                    #                     if isinstance(dep_resource, DockerResource):
                    #                         resources_to_update.append(dep_resource)
                    # Add the resources from the app to the list of resources to update
                    for app_resource in app_resources:
                        if isinstance(app_resource, DockerResource) and app_resource.should_update(
                            group_filter=group_filter, name_filter=name_filter, type_filter=type_filter
                        ):
                            resources_to_update.append(app_resource)

        # Sort the DockerResources in install order
        resources_to_update.sort(key=lambda x: DockerResourceInstallOrder.get(x.__class__.__name__, 5000), reverse=True)

        # Deduplicate DockerResources
        deduped_resources_to_update: List[DockerResource] = []
        for r in resources_to_update:
            if r not in deduped_resources_to_update:
                deduped_resources_to_update.append(r)

        # Implement dependency sorting
        final_docker_resources: List[DockerResource] = []
        logger.debug("-*- Building DockerResources dependency graph")
        for docker_resource in deduped_resources_to_update:
            # Logic to follow if resource has dependencies
            if docker_resource.depends_on is not None:
                # Add the dependencies before the resource itself
                for dep in docker_resource.depends_on:
                    if isinstance(dep, DockerResource):
                        if dep not in final_docker_resources:
                            logger.debug(f"-*- Adding {dep.name}, dependency of {docker_resource.name}")
                            final_docker_resources.append(dep)

                # Add the resource to be created after its dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)
            else:
                # Add the resource to be created if it has no dependencies
                if docker_resource not in final_docker_resources:
                    logger.debug(f"-*- Adding {docker_resource.name}")
                    final_docker_resources.append(docker_resource)

        # Track the total number of DockerResources to update for validation
        num_resources_to_update: int = len(final_docker_resources)
        num_resources_updated: int = 0
        if num_resources_to_update == 0:
            return 0, 0

        if dry_run:
            print_heading("--**- Docker resources to update:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_update} resources")
            return 0, 0

        # Validate resources to be updated
        if not auto_confirm:
            print_heading("\n--**-- Confirm resources to update:")
            for resource in final_docker_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            print_info(f"\nNetwork: {self.network}")
            print_info(f"Total {num_resources_to_update} resources")
            confirm = confirm_yes_no("\nConfirm patch")
            if not confirm:
                print_info("-*-")
                print_info("-*- Skipping update")
                print_info("-*-")
                return 0, 0

        for resource in final_docker_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            if isinstance(resource, DockerContainer):
                if resource.network is None and self.network is not None:
                    resource.network = self.network
            # logger.debug(resource)
            try:
                _resource_updated = resource.update(docker_client=self.docker_client)
                if _resource_updated:
                    num_resources_updated += 1
                else:
                    if self.workspace_settings is not None and not self.workspace_settings.continue_on_patch_failure:
                        return num_resources_updated, num_resources_to_update
            except Exception as e:
                logger.error(f"Failed to update {resource.get_resource_type()}: {resource.get_resource_name()}")
                logger.error(e)
                logger.error("Please fix and try again...")

        print_heading(f"\n--**-- Resources updated: {num_resources_updated}/{num_resources_to_update}")
        if num_resources_to_update != num_resources_updated:
            logger.error(
                f"Resources updated: {num_resources_updated} do not match resources required: {num_resources_to_update}"
            )  # noqa: E501
        return num_resources_updated, num_resources_to_update

    def save_resources(
        self,
        group_filter: Optional[str] = None,
        name_filter: Optional[str] = None,
        type_filter: Optional[str] = None,
        workspace_settings: Optional[WorkspaceSettings] = None,
    ) -> Tuple[int, int]:
        raise NotImplementedError
