from uuid import UUID

import psycopg
from psycopg import sql
from psycopg.rows import class_row
from pydantic import ValidationError
from src.__types.errors import (
    NonUniqueCustomerIdError,
    ResourceDuplicatedError,
    ResourceNotFoundError,
)
from src.__types.index import Distribution, Tenant, TenantAuthInfo
from src.settings import Settings

settings = Settings()


class TenantRepository:
    """Tenantを管理するリポジトリ"""

    def __init__(self, connection: psycopg.Connection) -> None:
        self.connection = connection

    def get(self, tenant_id: UUID) -> Tenant:
        """指定されたIDのTenantを取得する"""
        with self.connection.cursor(row_factory=class_row(Tenant)) as cur:
            cur.execute(
                """--sql
                SELECT
                    id,
                    m2m_application_client_id,
                    user_application_client_id,
                    legal_name,
                    name,
                    whi_customer_id,
                    cydas_customer_id,
                    allowed_m2m_metadata
                FROM tenants
                WHERE id = %(tenant_id)s
                """,
                {
                    "tenant_id": tenant_id,
                },
            )
            if tenant := cur.fetchone():
                return tenant

            raise ResourceNotFoundError(f"Tenant with ID {tenant_id} not found.")

    def get_by_m2m_application_client_id(self, m2m_application_client_id: str) -> Tenant:
        """指定されたM2MアプリケーションクライアントIDのテナントを取得する ※ 認可時にのみ使用"""
        with self.connection.cursor(row_factory=class_row(Tenant)) as cur:
            cur.execute(
                """--sql
                SELECT
                    id,
                    m2m_application_client_id,
                    user_application_client_id,
                    legal_name,
                    name,
                    whi_customer_id,
                    cydas_customer_id,
                    allowed_m2m_metadata
                FROM tenants
                WHERE m2m_application_client_id = %(m2m_application_client_id)s
                """,
                {
                    "m2m_application_client_id": m2m_application_client_id,
                },
            )
            if tenant := cur.fetchone():
                return tenant
            raise ResourceNotFoundError(
                f"Tenant with M2M application client ID '{m2m_application_client_id}' not found.",
            )

    def get_auth_info_by_name(self, name: str) -> TenantAuthInfo:
        """指定されたテナント名のユーザー認証情報を取得する"""
        with self.connection.cursor(row_factory=class_row(TenantAuthInfo)) as cur:
            cur.execute(
                """--sql
                SELECT
                    id AS tenant_id,
                    user_application_client_id
                FROM tenants
                WHERE name = %(name)s
                """,
                {
                    "name": name,
                },
            )

            try:
                if tenant_auth_info := cur.fetchone():
                    return tenant_auth_info
            except ValidationError as e:
                raise ValueError(
                    f"Tenant '{name}' has invalid auth info (e.g. missing application_client_id).",
                ) from e

            raise ResourceNotFoundError(f"Tenant with name '{name}' not found.")

    def get_by_customer_id(self, distribution: Distribution, customer_id: str) -> Tenant:
        """指定された商流の取引先IDに対応するTenantを取得する"""
        tenants = self._find_by_customer_id(distribution=distribution, customer_id=customer_id)
        if len(tenants) == 0:
            raise ResourceNotFoundError(
                f"Tenant with {distribution} customer ID '{customer_id}' not found.",
            )
        if len(tenants) == 1:
            return tenants[0]

        raise NonUniqueCustomerIdError(f"Customer ID `{customer_id}`is duplicated. Could not create a tenant.")

    def _find_by_customer_id(self, distribution: Distribution, customer_id: str) -> list[Tenant]:
        target_column = "whi_customer_id" if distribution == Distribution.WHI_SFDC else "cydas_customer_id"

        with self.connection.cursor(row_factory=class_row(Tenant)) as cur:
            cur.execute(
                sql.SQL(
                    """--sql
                    SELECT
                        id,
                        m2m_application_client_id,
                        user_application_client_id,
                        legal_name,
                        name,
                        whi_customer_id,
                        cydas_customer_id,
                        allowed_m2m_metadata
                    FROM tenants
                    WHERE %(customer_id)s = ANY({})
                    """,
                ).format(sql.Identifier(target_column)),
                {
                    "customer_id": customer_id,
                },
            )
            return list(cur.fetchall())

    def _validate_customer_ids(self, distribution: Distribution, customer_ids: list[str]) -> None:
        """distribution毎にcustomer_idが一意であることを追加時のロジックで保証する。"""
        # 同一リクエスト内での重複検知
        if duplicated_customer_id := self._find_duplicated_customer_id(customer_ids):
            raise ResourceDuplicatedError(
                f"Tenant with {distribution} customer ID '{duplicated_customer_id}' already exists.",
            )
        # 既存テナント内での重複検知
        for customer_id in customer_ids:
            if self._find_by_customer_id(distribution=distribution, customer_id=customer_id):
                raise ResourceDuplicatedError(
                    f"Tenant with {distribution} customer ID '{customer_id}' already exists.",
                )

    def _find_duplicated_customer_id(self, customer_ids: list[str]) -> str | None:
        seen_customer_ids: set[str] = set()
        for customer_id in customer_ids:
            if customer_id in seen_customer_ids:
                return customer_id
            seen_customer_ids.add(customer_id)
        return None

    def add(  # noqa: PLR0913
        self,
        tenant_id: UUID,  # NOTE: application_clientの命名規則の依存を断ち切った後にDB側で生成するようにする。
        legal_name: str,
        name: str | None,
        m2m_application_client_id: str,
        whi_customer_ids: list[str],
        cydas_customer_ids: list[str],
    ) -> None:
        """Tenantを追加する。"""
        self._validate_customer_ids(
            distribution=Distribution.WHI_SFDC,
            customer_ids=whi_customer_ids,
        )
        self._validate_customer_ids(
            distribution=Distribution.CYDAS_SFDC,
            customer_ids=cydas_customer_ids,
        )
        try:
            with self.connection.cursor() as cur:
                cur.execute(
                    """--sql
                    INSERT INTO tenants (
                        id,
                        m2m_application_client_id,
                        legal_name,
                        name,
                        whi_customer_id,
                        cydas_customer_id
                    ) VALUES (
                        %(tenant_id)s,
                        %(m2m_application_client_id)s,
                        %(legal_name)s,
                        %(name)s,
                        %(whi_customer_ids)s,
                        %(cydas_customer_ids)s
                    )
                    """,
                    {
                        "tenant_id": str(tenant_id),
                        "m2m_application_client_id": m2m_application_client_id,
                        "legal_name": legal_name,
                        "name": name,
                        "whi_customer_ids": whi_customer_ids or None,
                        "cydas_customer_ids": cydas_customer_ids or None,
                    },
                )
        except psycopg.errors.UniqueViolation as e:
            raise ResourceDuplicatedError("Tenant already exists.") from e

    def update_name_and_user_application_client_id(
        self,
        tenant_id: UUID,
        name: str | None,
        user_application_client_id: str | None,
    ) -> None:
        """Name と user_application_client_idを更新する。"""
        with self.connection.cursor() as cur:
            cur.execute(
                """--sql
                UPDATE tenants
                SET
                    name = %(name)s,
                    user_application_client_id = %(user_application_client_id)s
                WHERE id = %(tenant_id)s
                """,
                {
                    "tenant_id": str(tenant_id),
                    "name": name,
                    "user_application_client_id": user_application_client_id,
                },
            )
