from uuid import UUID

from psycopg import Connection
from src.__types.errors import BulkDuplicatedError, IdentityProviderError, ResourceDuplicatedError
from src.__types.index import Tenant, UserWriteAttribute
from src.domain.services.user_service import UserService
from src.infrastructure.clients.cognito_idp_client import CognitoIdpClient
from src.infrastructure.repositories.user_identity_repository import UserIdentityRepository
from src.infrastructure.repositories.user_repository import UserRepository
from src.logger import logger
from src.settings import Settings

settings = Settings()


class UserCommandUseCase:
    def __init__(
        self,
        user_repository: UserRepository,
        user_identity_repository: UserIdentityRepository,
    ) -> None:
        self.user_repository = user_repository
        self.user_identity_repository = user_identity_repository

    @classmethod
    def create(cls, tenant: Tenant, connection: Connection) -> "UserCommandUseCase":
        # Why: 複数routerから呼ばれるためインフラ依存の組み立てをここに集約する。
        if not settings.management_user_pool_id:
            raise ValueError("management_user_pool_id is required in settings")

        return UserCommandUseCase(
            user_repository=UserRepository(tenant=tenant, connection=connection),
            user_identity_repository=UserIdentityRepository(
                tenant=tenant,
                client=CognitoIdpClient(user_pool_id=settings.management_user_pool_id),
            ),
        )

    def add(self, display_id: str, display_name: str, email: str) -> None:
        """ユーザーを追加する。

        UserPoolのユーザー作成はDBのIDに依存するため、DB → UserPoolの順でユーザーを作成する。
        UserPoolのユーザー作成失敗時には、DBのユーザーを削除してロールバックする。
        """
        user = self.user_repository.create(
            display_id=display_id,
            display_name=display_name,
            email=email,
        )

        try:
            self.user_identity_repository.create_user(user=user)
        except (IdentityProviderError, ResourceDuplicatedError) as e:
            if isinstance(e, ResourceDuplicatedError):
                logger.error("UserPool failed: Ghost user detected. Manual cleanup required.")
            else:
                logger.warning("UserPool failed: Could not create user. Cleanup db user.")
            try:
                self.user_repository.bulk_delete(user_ids=[user.id])
            except Exception:  # noqa: BLE001 クリーンアップで発生した例外はログに残して握りつぶす
                logger.exception("Cleanup failed: Could not delete user from DB.", user_id=user.id)
            raise

    def bulk_add_validate(self, users: list[UserWriteAttribute]) -> None:
        """ユーザーの一括追加において、追加前に既存データとの重複を検証する。

        既存データとの重複を一括で検出し、全件分のエラー情報を返す。
        """
        display_ids = [u.display_id for u in users]
        emails = [u.email for u in users]

        existing = self.user_repository.find_duplicates(display_ids=display_ids, emails=emails)
        existing_display_ids = {u.display_id for u in existing}
        existing_emails = {u.email for u in existing}

        duplicated_fields: list[str] = []
        for i, user in enumerate(users):
            if user.display_id in existing_display_ids:
                duplicated_fields.append(f"users[{i}].display_id")
            if user.email in existing_emails:
                duplicated_fields.append(f"users[{i}].email")

        if duplicated_fields:
            raise BulkDuplicatedError(
                message="Duplicate users detected.",
                duplicated_fields=duplicated_fields,
            )

    def bulk_add(self, users: list[UserWriteAttribute]) -> None:
        """ユーザーを一括追加する。"""
        UserService(
            user_repository=self.user_repository,
            user_identity_repository=self.user_identity_repository,
        ).bulk_add(users=users)

    def update(self, user_id: UUID, display_id: str, display_name: str, email: str) -> None:
        """ユーザーを更新する。

        UserPoolのユーザー更新失敗時にDBが未変更で補償不要のため、UserPool → DBの順でユーザーを更新する。
        DB失敗時には、UserPoolのユーザーを旧データで更新しロールバックする。
        """
        userpool_updated = False
        old_user = self.user_repository.get(user_id)
        updated_user = old_user.model_copy(
            update={"display_id": display_id, "display_name": display_name, "email": email},
        )

        # 外部ID基盤の通信は高コストなため、ユーザーの属性に変更がない場合は更新処理をスキップする。
        if old_user.display_id == display_id and old_user.email == email:
            logger.debug("User attributes not changed, skipping identity update.", user_id=user_id)
        else:
            self.user_identity_repository.update_user(user=updated_user)
            userpool_updated = True

        try:
            self.user_repository.update(
                user_id=user_id,
                display_id=display_id,
                display_name=display_name,
                email=email,
            )
        except Exception:
            if userpool_updated:
                logger.warning("DB failed: Could not update user. Rollback UserPool user.", user_id=user_id)
                try:
                    self.user_identity_repository.update_user(user=old_user)
                except Exception:  # noqa: BLE001 クリーンアップで発生した例外はログに残して握りつぶす
                    logger.exception("Cleanup failed: Could not rollback user in UserPool.", user_id=user_id)
            raise

    def bulk_delete(self, user_ids: list[UUID]) -> None:
        """ユーザーを一括削除する。

        DB一括削除 → UserPool単体削除 の順で削除する。
        復元するとUserPool側で既に削除されたユーザーとの不整合が拡大するため、DBの復元は行わない。
        UserPool削除失敗時にはログに残し、手動での対応を促す。
        """
        self.user_repository.bulk_delete(user_ids=user_ids)

        failed_user_ids: list[UUID] = []
        for user_id in user_ids:
            try:
                self.user_identity_repository.delete_user(user_id=user_id)

            except Exception as e:  # noqa: PERF203 BLE001 どのエラーでもループを止めず残りのユーザーを処理する
                # NOTE: 最後にまとめてログに残すためwarningでログを残しつつ、失敗理由がわかるように例外情報もログに残す
                logger.warning("UserPool warning: Could not delete user.", exc_info=e, user_id=user_id)
                failed_user_ids.append(user_id)

        # NOTE: 日次バッチなどで定期的にUserPoolとDBのユーザーを突き合わせて不整合データを掃除することを検討する。
        if failed_user_ids:
            logger.error(
                "UserPool failed: Some users could not be deleted. Manual cleanup required.",
                failed_user_ids=failed_user_ids,
            )
            raise IdentityProviderError(f"Failed to delete users in UserPool. user_ids={failed_user_ids}")
