from typing import List, Optional, Union, Tuple

from phi.app.group import AppGroup
from phi.resource.group import ResourceGroup
from phi.aws.app.base import AwsApp
from phi.aws.app.context import AwsBuildContext
from phi.aws.api_client import AwsApiClient
from phi.aws.resource.base import AwsResource
from phi.infra.resources import InfraResources
from phi.utils.log import logger


class AwsResources(InfraResources):
    apps: Optional[List[Union[AwsApp, AppGroup]]] = None
    resources: Optional[List[Union[AwsResource, ResourceGroup]]] = None

    # -*- Cached Data
    _aws_region: Optional[str] = None
    _aws_profile: Optional[str] = None
    _api_client: Optional[AwsApiClient] = None

    @property
    def aws_region(self) -> Optional[str]:
        # Priority 1: Use aws_region from ResourceGroup (or cached value)
        if self._aws_region:
            return self._aws_region

        # Priority 2: Get aws_region from workspace settings
        if self.workspace_settings is not None and self.workspace_settings.aws_region is not None:
            self._aws_region = self.workspace_settings.aws_region
            return self._aws_region

        # Priority 3: Get aws_region from env
        from os import getenv
        from phi.constants import AWS_REGION_ENV_VAR

        aws_region_env = getenv(AWS_REGION_ENV_VAR)
        if aws_region_env is not None:
            logger.debug(f"{AWS_REGION_ENV_VAR}: {aws_region_env}")
            self._aws_region = aws_region_env
        return self._aws_region

    @aws_region.setter
    def aws_region(self, value: str) -> None:
        self._aws_region = value

    @property
    def aws_profile(self) -> Optional[str]:
        # Priority 1: Use aws_region from ResourceGroup (or cached value)
        if self._aws_profile:
            return self._aws_profile

        # Priority 2: Get aws_profile from workspace settings
        if self.workspace_settings is not None and self.workspace_settings.aws_profile is not None:
            self._aws_profile = self.workspace_settings.aws_profile
            return self._aws_profile

        # Priority 3: Get aws_profile from env
        from os import getenv
        from phi.constants import AWS_PROFILE_ENV_VAR

        aws_profile_env = getenv(AWS_PROFILE_ENV_VAR)
        if aws_profile_env is not None:
            logger.debug(f"{AWS_PROFILE_ENV_VAR}: {aws_profile_env}")
            self._aws_profile = aws_profile_env
        return self._aws_profile

    @aws_profile.setter
    def aws_profile(self, value: str) -> None:
        self._aws_profile = value

    @property
    def aws_client(self) -> AwsApiClient:
        if self._api_client is None:
            self._api_client = AwsApiClient(aws_region=self.aws_region, aws_profile=self.aws_profile)
        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.aws.resource.types import AwsResourceInstallOrder

        logger.debug("-*- Creating AwsResources")
        # Build a list of AwsResources to create
        resources_to_create: List[AwsResource] = []
        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, AwsResource):
                                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, AwsResource):
                    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 AwsApps to create
        apps_to_create: List[AwsApp] = []
        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, AwsApp):
                                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, AwsApp):
                    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 AwsResources from the AwsApps
        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=AwsBuildContext(aws_region=self.aws_region, aws_profile=self.aws_profile)
                )
                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, AwsApp):
                                dep.set_workspace_settings(workspace_settings=self.workspace_settings)
                                dep_resources = dep.get_resources(
                                    build_context=AwsBuildContext(
                                        aws_region=self.aws_region, aws_profile=self.aws_profile
                                    )
                                )
                                if len(dep_resources) > 0:
                                    for dep_resource in dep_resources:
                                        if isinstance(dep_resource, AwsResource):
                                            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, AwsResource) 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 AwsResources in install order
        resources_to_create.sort(key=lambda x: AwsResourceInstallOrder.get(x.__class__.__name__, 5000))

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

        # Implement dependency sorting
        final_aws_resources: List[AwsResource] = []
        logger.debug("-*- Building AwsResources dependency graph")
        for aws_resource in deduped_resources_to_create:
            # Logic to follow if resource has dependencies
            if aws_resource.depends_on is not None and len(aws_resource.depends_on) > 0:
                # Add the dependencies before the resource itself
                for dep in aws_resource.depends_on:
                    if isinstance(dep, AwsResource):
                        if dep not in final_aws_resources:
                            logger.debug(f"-*- Adding {dep.name}, dependency of {aws_resource.name}")
                            final_aws_resources.append(dep)

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

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

        if dry_run:
            print_heading("--**- AWS resources to create:")
            for resource in final_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            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_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            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_aws_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            # logger.debug(resource)
            try:
                _resource_created = resource.create(aws_client=self.aws_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.aws.resource.types import AwsResourceInstallOrder

        logger.debug("-*- Deleting AwsResources")

        # Build a list of AwsResources to delete
        resources_to_delete: List[AwsResource] = []
        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, AwsResource):
                                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, AwsResource):
                    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 AwsApps to delete
        apps_to_delete: List[AwsApp] = []
        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, AwsApp):
                                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, AwsApp):
                    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 AwsResources from the AwsApps
        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=AwsBuildContext(aws_region=self.aws_region, aws_profile=self.aws_profile)
                )
                if len(app_resources) > 0:
                    for app_resource in app_resources:
                        if isinstance(app_resource, AwsResource) and app_resource.should_delete(
                            group_filter=group_filter, name_filter=name_filter, type_filter=type_filter
                        ):
                            resources_to_delete.append(app_resource)

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

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

        # Implement dependency sorting
        final_aws_resources: List[AwsResource] = []
        logger.debug("-*- Building AwsResources dependency graph")
        for aws_resource in deduped_resources_to_delete:
            # Logic to follow if resource has dependencies
            if aws_resource.depends_on is not None and len(aws_resource.depends_on) > 0:
                # 1. Reverse the order of dependencies
                aws_resource.depends_on.reverse()

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

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

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

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

        if dry_run:
            print_heading("--**- AWS resources to delete:")
            for resource in final_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            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_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            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_aws_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            # logger.debug(resource)
            try:
                _resource_deleted = resource.delete(aws_client=self.aws_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.aws.resource.types import AwsResourceInstallOrder

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

        # Build a list of AwsResources to update
        resources_to_update: List[AwsResource] = []
        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, AwsResource):
                                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, AwsResource):
                    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 AwsApps to update
        apps_to_update: List[AwsApp] = []
        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, AwsApp):
                                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, AwsApp):
                    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 AwsResources from the AwsApps
        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=AwsBuildContext(aws_region=self.aws_region, aws_profile=self.aws_profile)
                )
                if len(app_resources) > 0:
                    for app_resource in app_resources:
                        if isinstance(app_resource, AwsResource) 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 AwsResources in install order
        resources_to_update.sort(key=lambda x: AwsResourceInstallOrder.get(x.__class__.__name__, 5000))

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

        # Implement dependency sorting
        final_aws_resources: List[AwsResource] = []
        logger.debug("-*- Building AwsResources dependency graph")
        for aws_resource in deduped_resources_to_update:
            # Logic to follow if resource has dependencies
            if aws_resource.depends_on is not None and len(aws_resource.depends_on) > 0:
                # Add the dependencies before the resource itself
                for dep in aws_resource.depends_on:
                    if isinstance(dep, AwsResource):
                        if dep not in final_aws_resources:
                            logger.debug(f"-*- Adding {dep.name}, dependency of {aws_resource.name}")
                            final_aws_resources.append(dep)

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

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

        if dry_run:
            print_heading("--**- AWS resources to update:")
            for resource in final_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            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_aws_resources:
                print_info(f"  -+-> {resource.get_resource_type()}: {resource.get_resource_name()}")
            print_info("")
            if self.aws_region:
                print_info(f"Region: {self.aws_region}")
            if self.aws_profile:
                print_info(f"Profile: {self.aws_profile}")
            print_info(f"Total {num_resources_to_update} resources")
            confirm = confirm_yes_no("\nConfirm patch")
            if not confirm:
                print_info("-*-")
                print_info("-*- Skipping patch")
                print_info("-*-")
                return 0, 0

        for resource in final_aws_resources:
            print_info(f"\n-==+==- {resource.get_resource_type()}: {resource.get_resource_name()}")
            if force is True:
                resource.force = True
            # logger.debug(resource)
            try:
                _resource_updated = resource.update(aws_client=self.aws_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,
    ) -> Tuple[int, int]:
        raise NotImplementedError
