"""Phase 6.1 supporting services: address, tag, custom field, note, attachment, lookups."""
from __future__ import annotations

from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import CrmEventType
from app.models.supporting import (
    ContactTag,
    ContactType,
    CrmAddress,
    CrmAttachment,
    CrmCustomField,
    CrmCustomFieldValue,
    CrmNote,
    CrmNoteVersion,
    CrmTag,
    LeadSource,
    LeadStatusDefinition,
    LeadTag,
    OrganizationIndustry,
    OrganizationTag,
    OrganizationType,
)
from app.models.types import AuditAction, CrmEntityType
from app.repositories.supporting import (
    ContactTagRepository,
    ContactTypeRepository,
    CrmAddressRepository,
    CrmAttachmentRepository,
    CrmCustomFieldRepository,
    CrmCustomFieldValueRepository,
    CrmNoteRepository,
    CrmNoteVersionRepository,
    CrmTagRepository,
    LeadSourceRepository,
    LeadStatusDefinitionRepository,
    LeadTagRepository,
    OrganizationIndustryRepository,
    OrganizationTagRepository,
    OrganizationTypeRepository,
)
from app.schemas.foundation import (
    AddressCreate,
    AddressUpdate,
    AttachmentCreate,
    CustomFieldCreate,
    CustomFieldValueUpsert,
    LeadStatusCreate,
    LookupCreate,
    NoteCreate,
    NoteUpdate,
    TagCreate,
)
from app.services.audit_service import AuditService
from app.validators import (
    validate_address_line,
    validate_attachment_file_name,
    validate_custom_field_value,
    validate_file_storage_id,
    validate_non_empty,
    validate_tag_name,
)
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)


class LookupService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.lead_sources = LeadSourceRepository(session)
        self.lead_statuses = LeadStatusDefinitionRepository(session)
        self.contact_types = ContactTypeRepository(session)
        self.org_types = OrganizationTypeRepository(session)
        self.org_industries = OrganizationIndustryRepository(session)

    async def create_lead_source(self, tenant_id: UUID, body: LookupCreate) -> LeadSource:
        code = validate_non_empty(body.code, field="code")
        if await self.lead_sources.get_by_code(tenant_id, code):
            raise AppError("کد منبع تکراری است", status_code=409, error_code="code_exists")
        entity = LeadSource(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            is_active=body.is_active,
        )
        await self.lead_sources.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_lead_sources(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.lead_sources.list_by_tenant(
            tenant_id, offset=offset, limit=limit
        )

    async def create_lead_status(
        self, tenant_id: UUID, body: LeadStatusCreate
    ) -> LeadStatusDefinition:
        code = validate_non_empty(body.code, field="code")
        if await self.lead_statuses.get_by_code(tenant_id, code):
            raise AppError("کد وضعیت تکراری است", status_code=409, error_code="code_exists")
        entity = LeadStatusDefinition(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            position=body.position,
            is_terminal=body.is_terminal,
            is_converted=body.is_converted,
            is_active=body.is_active,
            color=body.color,
        )
        await self.lead_statuses.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_lead_statuses(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.lead_statuses.list_by_tenant(
            tenant_id, offset=offset, limit=limit
        )

    async def create_contact_type(self, tenant_id: UUID, body: LookupCreate) -> ContactType:
        code = validate_non_empty(body.code, field="code")
        if await self.contact_types.get_by_code(tenant_id, code):
            raise AppError("کد نوع مخاطب تکراری است", status_code=409, error_code="code_exists")
        entity = ContactType(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            is_active=body.is_active,
        )
        await self.contact_types.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_contact_types(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.contact_types.list_by_tenant(
            tenant_id, offset=offset, limit=limit
        )

    async def create_organization_type(
        self, tenant_id: UUID, body: LookupCreate
    ) -> OrganizationType:
        code = validate_non_empty(body.code, field="code")
        if await self.org_types.get_by_code(tenant_id, code):
            raise AppError("کد نوع سازمان تکراری است", status_code=409, error_code="code_exists")
        entity = OrganizationType(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            is_active=body.is_active,
        )
        await self.org_types.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_organization_types(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.org_types.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def create_organization_industry(
        self, tenant_id: UUID, body: LookupCreate
    ) -> OrganizationIndustry:
        code = validate_non_empty(body.code, field="code")
        if await self.org_industries.get_by_code(tenant_id, code):
            raise AppError("کد صنعت تکراری است", status_code=409, error_code="code_exists")
        entity = OrganizationIndustry(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            is_active=body.is_active,
        )
        await self.org_industries.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_organization_industries(
        self, tenant_id: UUID, *, offset: int, limit: int
    ):
        return await self.org_industries.list_by_tenant(
            tenant_id, offset=offset, limit=limit
        )


class AddressService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = CrmAddressRepository(session)
        self.audit = AuditService(session)

    async def create(
        self,
        tenant_id: UUID,
        body: AddressCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> CrmAddress:
        actor_id = actor.user_id if actor else None
        line1 = validate_address_line(body.line1)
        if body.is_default:
            for existing in await self.repo.list_for_entity(
                tenant_id, body.entity_type, body.entity_id
            ):
                existing.is_default = False
        entity = CrmAddress(
            tenant_id=tenant_id,
            **{**body.model_dump(), "line1": line1},
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="address",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def update(
        self,
        tenant_id: UUID,
        address_id: UUID,
        body: AddressUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> CrmAddress:
        entity = await self.repo.get(tenant_id, address_id)
        if entity is None:
            raise NotFoundError("آدرس یافت نشد", error_code="address_not_found")
        data = body.model_dump(exclude_unset=True)
        if "line1" in data:
            data["line1"] = validate_address_line(data["line1"])
        if data.get("is_default") is True:
            for existing in await self.repo.list_for_entity(
                tenant_id, entity.entity_type, entity.entity_id
            ):
                if existing.id != address_id:
                    existing.is_default = False
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_for_entity(
        self, tenant_id: UUID, entity_type: CrmEntityType, entity_id: UUID
    ):
        return await self.repo.list_for_entity(tenant_id, entity_type, entity_id)

    async def get(self, tenant_id: UUID, address_id: UUID) -> CrmAddress:
        entity = await self.repo.get(tenant_id, address_id)
        if entity is None:
            raise NotFoundError("آدرس یافت نشد", error_code="address_not_found")
        return entity


class TagService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = CrmTagRepository(session)
        self.lead_tags = LeadTagRepository(session)
        self.contact_tags = ContactTagRepository(session)
        self.org_tags = OrganizationTagRepository(session)

    async def create(self, tenant_id: UUID, body: TagCreate) -> CrmTag:
        name = validate_tag_name(body.name)
        if await self.repo.get_by_name(tenant_id, name):
            raise AppError("تگ تکراری است", status_code=409, error_code="tag_exists")
        entity = CrmTag(
            tenant_id=tenant_id,
            name=name,
            color=body.color,
            group_name=body.group_name,
            scope=body.scope,
            is_active=body.is_active,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def assign_to_lead(self, tenant_id: UUID, lead_id: UUID, tag_id: UUID):
        tag = await self.repo.get(tenant_id, tag_id)
        if tag is None:
            raise NotFoundError("تگ یافت نشد", error_code="tag_not_found")
        existing = await self.lead_tags.find(tenant_id, lead_id, tag_id)
        if existing:
            return existing
        row = LeadTag(tenant_id=tenant_id, lead_id=lead_id, tag_id=tag_id)
        await self.lead_tags.add(row)
        await self.session.commit()
        await self.session.refresh(row)
        return row

    async def assign_to_contact(self, tenant_id: UUID, contact_id: UUID, tag_id: UUID):
        tag = await self.repo.get(tenant_id, tag_id)
        if tag is None:
            raise NotFoundError("تگ یافت نشد", error_code="tag_not_found")
        existing = await self.contact_tags.find(tenant_id, contact_id, tag_id)
        if existing:
            return existing
        row = ContactTag(tenant_id=tenant_id, contact_id=contact_id, tag_id=tag_id)
        await self.contact_tags.add(row)
        await self.session.commit()
        await self.session.refresh(row)
        return row

    async def assign_to_organization(
        self, tenant_id: UUID, organization_id: UUID, tag_id: UUID
    ):
        tag = await self.repo.get(tenant_id, tag_id)
        if tag is None:
            raise NotFoundError("تگ یافت نشد", error_code="tag_not_found")
        existing = await self.org_tags.find(tenant_id, organization_id, tag_id)
        if existing:
            return existing
        row = OrganizationTag(
            tenant_id=tenant_id, organization_id=organization_id, tag_id=tag_id
        )
        await self.org_tags.add(row)
        await self.session.commit()
        await self.session.refresh(row)
        return row


class CustomFieldService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = CrmCustomFieldRepository(session)
        self.values = CrmCustomFieldValueRepository(session)

    async def create(self, tenant_id: UUID, body: CustomFieldCreate) -> CrmCustomField:
        key = validate_non_empty(body.field_key, field="field_key")
        if await self.repo.get_by_key(tenant_id, body.entity_type, key):
            raise AppError(
                "کلید فیلد سفارشی تکراری است",
                status_code=409,
                error_code="custom_field_exists",
            )
        entity = CrmCustomField(
            tenant_id=tenant_id,
            entity_type=body.entity_type,
            field_key=key,
            label=validate_non_empty(body.label, field="label"),
            field_type=body.field_type,
            is_required=body.is_required,
            options=body.options,
            validation_rules=body.validation_rules,
            position=body.position,
            is_active=body.is_active,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_definitions(
        self, tenant_id: UUID, entity_type: CrmEntityType
    ):
        return await self.repo.list_by_entity_type(tenant_id, entity_type)

    async def upsert_value(
        self, tenant_id: UUID, body: CustomFieldValueUpsert
    ) -> CrmCustomFieldValue:
        field = await self.repo.get(tenant_id, body.custom_field_id)
        if field is None:
            raise NotFoundError(
                "فیلد سفارشی یافت نشد", error_code="custom_field_not_found"
            )
        validate_custom_field_value(
            field_type=field.field_type,
            is_required=field.is_required,
            options=field.options,
            value_text=body.value_text,
            value_number=body.value_number,
            value_boolean=body.value_boolean,
            value_date=body.value_date,
            value_json=body.value_json,
        )
        existing_rows = await self.values.list_for_entity(
            tenant_id, body.entity_type, body.entity_id
        )
        for row in existing_rows:
            if row.custom_field_id == body.custom_field_id:
                row.value_text = body.value_text
                row.value_number = body.value_number
                row.value_boolean = body.value_boolean
                row.value_date = body.value_date
                row.value_json = body.value_json
                await self.session.commit()
                await self.session.refresh(row)
                return row
        entity = CrmCustomFieldValue(
            tenant_id=tenant_id,
            custom_field_id=body.custom_field_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            value_text=body.value_text,
            value_number=body.value_number,
            value_boolean=body.value_boolean,
            value_date=body.value_date,
            value_json=body.value_json,
        )
        await self.values.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class NoteService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = CrmNoteRepository(session)
        self.versions = CrmNoteVersionRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: NoteCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> CrmNote:
        actor_id = actor.user_id if actor else None
        body_text = validate_non_empty(body.body, field="body")
        entity = CrmNote(
            tenant_id=tenant_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            body=body_text,
            body_format=body.body_format,
            version=1,
            is_pinned=body.is_pinned,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.versions.add(
            CrmNoteVersion(
                tenant_id=tenant_id,
                note_id=entity.id,
                version=1,
                body=body_text,
                created_by=actor_id,
            )
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="note",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.NOTE_CREATED,
            aggregate_type="note",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "entity_type": entity.entity_type.value,
                "entity_id": str(entity.entity_id),
            },
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        note_id: UUID,
        body: NoteUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> CrmNote:
        entity = await self.repo.get(tenant_id, note_id)
        if entity is None:
            raise NotFoundError("یادداشت یافت نشد", error_code="note_not_found")
        actor_id = actor.user_id if actor else None
        data = body.model_dump(exclude_unset=True)
        if "body" in data and data["body"] is not None:
            data["body"] = validate_non_empty(data["body"], field="body")
            entity.version += 1
            await self.versions.add(
                CrmNoteVersion(
                    tenant_id=tenant_id,
                    note_id=entity.id,
                    version=entity.version,
                    body=data["body"],
                    created_by=actor_id,
                )
            )
        _apply_update(entity, data)
        entity.updated_by = actor_id
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_for_entity(
        self, tenant_id: UUID, entity_type: CrmEntityType, entity_id: UUID
    ):
        return await self.repo.list_for_entity(tenant_id, entity_type, entity_id)


class AttachmentService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = CrmAttachmentRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: AttachmentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> CrmAttachment:
        actor_id = actor.user_id if actor else None
        entity = CrmAttachment(
            tenant_id=tenant_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            file_storage_id=validate_file_storage_id(body.file_storage_id),
            file_name=validate_attachment_file_name(body.file_name),
            content_type=body.content_type,
            size_bytes=body.size_bytes,
            kind=body.kind,
            description=body.description,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="attachment",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.ATTACHMENT_ADDED,
            aggregate_type="attachment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "file_storage_id": entity.file_storage_id,
                "file_name": entity.file_name,
            },
        )
        return entity

    async def list_for_entity(
        self, tenant_id: UUID, entity_type: CrmEntityType, entity_id: UUID
    ):
        return await self.repo.list_for_entity(tenant_id, entity_type, entity_id)
