"""Healthcare foundation application services — Phase 13.0."""
from __future__ import annotations

from datetime import date, datetime
from typing import Any
from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import HealthcareEventType
from app.models.foundation import (
    Branch,
    Clinic,
    Department,
    ExternalProviderConfig,
    HealthcareConfiguration,
    HealthcareSetting,
)
from app.models.types import AuditAction
from app.repositories.foundation import (
    BranchRepository,
    ClinicRepository,
    DepartmentRepository,
    ExternalProviderConfigRepository,
    HealthcareConfigurationRepository,
    HealthcareSettingRepository,
)
from app.schemas.foundation import (
    BranchCreate,
    BranchUpdate,
    ClinicCreate,
    ClinicUpdate,
    DepartmentCreate,
    DepartmentUpdate,
    ExternalProviderConfigCreate,
    ExternalProviderConfigUpdate,
    HealthcareConfigurationCreate,
    HealthcareConfigurationUpdate,
    HealthcareSettingUpsert,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    validate_code,
    validate_currency_code,
    validate_non_empty,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


def _jsonable_changes(data: dict[str, Any]) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for key, value in data.items():
        if isinstance(value, UUID):
            out[key] = str(value)
        elif isinstance(value, (datetime, date)):
            out[key] = value.isoformat()
        elif hasattr(value, "value"):
            out[key] = value.value
        else:
            out[key] = value
    return out


def _actor_id(actor: CurrentUser | None) -> str | None:
    return actor.user_id if actor else None


class ClinicService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ClinicRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ClinicCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Clinic:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        currency = validate_currency_code(body.currency_code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد کلینیک تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = Clinic(
            tenant_id=tenant_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            legal_name=body.legal_name,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            settings=body.settings,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد کلینیک تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="clinic",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.CLINIC_CREATED,
            aggregate_type="clinic",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "name": entity.name},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Clinic:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("کلینیک یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[Clinic]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ClinicUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Clinic:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="clinic",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=HealthcareEventType.CLINIC_UPDATED,
            aggregate_type="clinic",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> Clinic:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="clinic",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class BranchService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = BranchRepository(session)
        self.clinics = ClinicRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: BranchCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Branch:
        clinic = await self.clinics.get(tenant_id, body.clinic_id)
        if clinic is None:
            raise NotFoundError("کلینیک یافت نشد")
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        entity = Branch(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            address=body.address,
            timezone=body.timezone,
            working_hours=body.working_hours,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد شعبه تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="branch",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.BRANCH_CREATED,
            aggregate_type="branch",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "clinic_id": str(entity.clinic_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Branch:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("شعبه یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[Branch]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: BranchUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="branch",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=HealthcareEventType.BRANCH_UPDATED,
            aggregate_type="branch",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="branch",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class DepartmentService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = DepartmentRepository(session)
        self.clinics = ClinicRepository(session)
        self.branches = BranchRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: DepartmentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Department:
        clinic = await self.clinics.get(tenant_id, body.clinic_id)
        if clinic is None:
            raise NotFoundError("کلینیک یافت نشد")
        if body.branch_id is not None:
            branch = await self.branches.get(tenant_id, body.branch_id)
            if branch is None:
                raise NotFoundError("شعبه یافت نشد")
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        entity = Department(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            branch_id=body.branch_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            specialty=body.specialty,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد بخش تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="department",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.DEPARTMENT_CREATED,
            aggregate_type="department",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "clinic_id": str(entity.clinic_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Department:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("بخش یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[Department]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: DepartmentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Department:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        if "branch_id" in data and data["branch_id"] is not None:
            branch = await self.branches.get(tenant_id, data["branch_id"])
            if branch is None:
                raise NotFoundError("شعبه یافت نشد")
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="department",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=HealthcareEventType.DEPARTMENT_UPDATED,
            aggregate_type="department",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> Department:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="department",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExternalProviderConfigService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExternalProviderConfigRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExternalProviderConfigCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد ارائه‌دهنده تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExternalProviderConfig(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            code=code,
            name=name,
            provider_kind=body.provider_kind,
            adapter_key=adapter_key,
            status=body.status,
            priority=body.priority,
            credentials_ref=body.credentials_ref,
            config=body.config,
            capabilities=body.capabilities,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.REGISTER,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.EXTERNAL_PROVIDER_REGISTERED,
            aggregate_type="external_provider_config",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "adapter_key": entity.adapter_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ExternalProviderConfig:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("ارائه‌دهنده یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExternalProviderConfig]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExternalProviderConfigUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        if "adapter_key" in data and data["adapter_key"] is not None:
            data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=HealthcareEventType.EXTERNAL_PROVIDER_UPDATED,
            aggregate_type="external_provider_config",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class HealthcareConfigurationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = HealthcareConfigurationRepository(session)
        self.clinics = ClinicRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: HealthcareConfigurationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> HealthcareConfiguration:
        clinic = await self.clinics.get(tenant_id, body.clinic_id)
        if clinic is None:
            raise NotFoundError("کلینیک یافت نشد")
        code = validate_code(body.code)
        currency = validate_currency_code(body.currency_code)
        entity = HealthcareConfiguration(
            tenant_id=tenant_id,
            clinic_id=body.clinic_id,
            branch_id=body.branch_id,
            code=code,
            working_hours=body.working_hours,
            clinical_policies=body.clinical_policies,
            privacy_policies=body.privacy_policies,
            custom_fields=body.custom_fields,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد پیکربندی تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="healthcare_configuration",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.CONFIGURATION_CREATED,
            aggregate_type="healthcare_configuration",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HealthcareConfiguration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پیکربندی یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[HealthcareConfiguration]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: HealthcareConfigurationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> HealthcareConfiguration:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="healthcare_configuration",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=HealthcareEventType.CONFIGURATION_UPDATED,
            aggregate_type="healthcare_configuration",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> HealthcareConfiguration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="healthcare_configuration",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class HealthcareSettingService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = HealthcareSettingRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def upsert(
        self,
        tenant_id: UUID,
        body: HealthcareSettingUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> HealthcareSetting:
        key = validate_non_empty(body.key, "key")
        existing = await self.repo.get_by_key(
            tenant_id,
            key,
            clinic_id=body.clinic_id,
            branch_id=body.branch_id,
        )
        if existing is None:
            entity = HealthcareSetting(
                tenant_id=tenant_id,
                clinic_id=body.clinic_id,
                branch_id=body.branch_id,
                key=key,
                value=body.value,
                description=body.description,
                created_by=_actor_id(actor),
                updated_by=_actor_id(actor),
            )
            await self.repo.add(entity)
        else:
            entity = existing
            entity.value = body.value
            entity.description = body.description
            entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="healthcare_setting",
            entity_id=entity.id,
            action=AuditAction.UPDATE if existing else AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=HealthcareEventType.SETTING_UPDATED,
            aggregate_type="healthcare_setting",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"key": entity.key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[HealthcareSetting]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
