import uuid

from psycopg import Connection
from src.__types.errors import ClientError, ResourceDuplicatedError, ResourceNotFoundError
from src.__types.index import ContractProvider, Distribution, Tenant
from src.domain.entities.contract_event import ContractEvent
from src.domain.entities.quota import Quota
from src.domain.entities.quota_allocation import QuotaAllocation, QuotaAllocationStatus, QuotaAllocationType
from src.domain.services.tenant_service import TenantService
from src.infrastructure.contract_event_notifier import ContractEventNotifierProtocol
from src.infrastructure.credential_manager import CredentialManager
from src.infrastructure.database.execute_tenant_rls import execute_tenant_rls
from src.infrastructure.repositories.quota_repository import QuotaRepository
from src.infrastructure.repositories.tenant_repository import TenantRepository
from src.logger import logger


class ContractEventUseCase:
    def __init__(
        self,
        connection: Connection,
        tenant_service: TenantService,
        tenant_repository: TenantRepository,
        notifier: ContractEventNotifierProtocol,
    ) -> None:
        self.connection = connection
        self.tenant_service = tenant_service
        self.tenant_repository = tenant_repository
        self.notifier = notifier

    @classmethod
    def create(
        cls,
        connection: Connection,
        credential_manager: CredentialManager,
        notifier: ContractEventNotifierProtocol,
    ) -> "ContractEventUseCase":
        return ContractEventUseCase(
            connection=connection,
            tenant_service=TenantService.create(
                connection=connection,
                credential_manager=credential_manager,
            ),
            tenant_repository=TenantRepository(connection=connection),
            notifier=notifier,
        )

    def execute(self, contract_provider: ContractProvider, contract_event: ContractEvent) -> None:
        """ユニット契約に基づいて、テナント発行（未発行の場合）・クォータ割り当てを行う"""
        # 商流取り違え防止のため、イベントのdistributionとプロバイダに紐づくdistributionが一致するか検証
        self._validate_provider_distribution(
            provider_name=contract_provider.name,
            event_distribution=contract_event.distribution,
        )

        tenant = self._get_or_issue_tenant(contract_event=contract_event)

        logger.info(f"target tenant_id: {tenant.id}")
        ## テナントがこのタイミングで初めて確定するため、クォータの割り当て前にtenantごとのRLSを有効化する
        execute_tenant_rls(connection=self.connection, tenant_id=tenant.id)

        self._add_quota_allocation(tenant=tenant, contract_event=contract_event)

    # TODO： WHI・CYDASのSFDCが統合された場合、プロバイダとDistributionの検証ロジックを再検討する
    @classmethod
    def _validate_provider_distribution(
        cls,
        provider_name: str,
        event_distribution: Distribution,
    ) -> None:
        try:
            expected_distribution = Distribution(provider_name)
        except ValueError as e:
            raise ClientError(f"Unsupported contract provider name: {provider_name}") from e

        if event_distribution != expected_distribution:
            raise ClientError(
                "Distribution does not match contract provider. "
                f"provider={provider_name}, distribution={event_distribution.value}",
            )

    def _get_or_issue_tenant(self, contract_event: ContractEvent) -> Tenant:
        """契約イベントに紐づくテナントを取得し、存在しない場合は発行する"""
        try:
            tenant = self.tenant_repository.get_by_customer_id(
                distribution=contract_event.distribution,
                customer_id=contract_event.customer_id,
            )
            logger.info(f"Tenant found: id={tenant.id}")
        except ResourceNotFoundError:
            return self._issue_tenant(contract_event=contract_event)
        else:
            return tenant

    def _issue_tenant(self, contract_event: ContractEvent) -> Tenant:
        """契約イベントに紐づくテナントを発行する"""
        whi_customer_ids = [contract_event.customer_id] if contract_event.distribution == Distribution.WHI_SFDC else []
        cydas_customer_ids = (
            [contract_event.customer_id] if contract_event.distribution == Distribution.CYDAS_SFDC else []
        )

        try:
            tenant = self.tenant_service.issue(
                legal_name=contract_event.legal_name,
                name=None,  # TODO: SFDCから送られてくるパラメータ確定次第修正、現状はダミー値
                whi_customer_ids=whi_customer_ids,
                cydas_customer_ids=cydas_customer_ids,
            )
            logger.info(
                f"Tenant issued successfully: tenant_id={tenant.id}, customer_id={contract_event.customer_id}",
            )
        except ResourceDuplicatedError:
            tenant = self.tenant_repository.get_by_customer_id(
                distribution=contract_event.distribution,
                customer_id=contract_event.customer_id,
            )
            logger.info(f"Tenant already exists (race condition): tenant_id={tenant.id}")
            return tenant
        except Exception as e:
            self.notifier.notify_tenant_creation_failed(
                contract_event=contract_event,
                error_message=str(e),
            )
            raise
        else:
            self.notifier.notify_tenant_created(tenant=tenant, contract_event=contract_event)
            return tenant

    # TODO: 基本契約を複数持てるようにする場合に削除する
    @staticmethod
    def _has_overlapping_base_allocation(quota: Quota, new_allocation: QuotaAllocation) -> bool:
        """期間が重複するACTIVEまたはSCHEDULEDな基本契約が既に存在するか判定する"""
        return any(
            allocation.type == QuotaAllocationType.BASE
            and allocation.status in (QuotaAllocationStatus.ACTIVE, QuotaAllocationStatus.SCHEDULED)
            and allocation.valid_from_date <= new_allocation.valid_to_date
            and new_allocation.valid_from_date <= allocation.valid_to_date
            for allocation in quota.allocations
        )

    def _add_quota_allocation(self, tenant: Tenant, contract_event: ContractEvent) -> None:
        allocation = QuotaAllocation.create(
            id=uuid.uuid4(),
            type=contract_event.type,
            amount=contract_event.amount,
            valid_from_date=contract_event.valid_from_date,
            valid_to_date=contract_event.valid_to_date,
        )

        quota_repository = QuotaRepository(tenant=tenant, connection=self.connection)

        try:
            # TODO: 基本契約を複数持てるようにする場合に削除する
            if allocation.type == QuotaAllocationType.BASE and self._has_overlapping_base_allocation(
                quota_repository.get(),
                allocation,
            ):
                raise ClientError(  # noqa: TRY301
                    "A base allocation with an overlapping period already exists for this tenant.",
                )

            quota_repository.add_quota_allocation(
                allocation,
                contract_id=contract_event.contract_id,
            )
            logger.info(
                f"QuotaAllocation added: tenant_id={tenant.id}, contract_id={contract_event.contract_id}",
                allocation={
                    "id": str(allocation.id),
                    "allocation_type": allocation.type.value,
                    "valid_from_date": str(allocation.valid_from_date),
                    "valid_to_date": str(allocation.valid_to_date),
                },
            )
        except ResourceDuplicatedError:
            logger.info(
                "QuotaAllocation already exists. Skipping.",
                extra={
                    "contract_id": contract_event.contract_id,
                    "valid_from_date": contract_event.valid_from_date,
                    "valid_to_date": contract_event.valid_to_date,
                },
            )
            return
        except ClientError as e:
            logger.exception(e.message)
            raise
        except Exception as e:
            self.notifier.notify_quota_allocation_failed(
                tenant=tenant,
                contract_event=contract_event,
                error_message=str(e),
            )
            raise
        else:
            self.notifier.notify_quota_allocation_added(
                tenant=tenant,
                allocation=allocation,
                contract_id=contract_event.contract_id,
            )
