"""Manual contacts and dynamic contact source resolution — no business data duplication."""
from __future__ import annotations

import csv
import io
from typing import Any
from uuid import UUID

import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.foundation import ContactSource, ManualContact
from app.repositories.foundation import ContactSourceRepository, ManualContactRepository
from app.validators import validate_contact, validate_contact_source
from shared.exceptions import AppError, NotFoundError


class ResolvedContact:
    def __init__(
        self,
        *,
        address: str,
        channel_hint: str | None = None,
        display_name: str | None = None,
        external_ref: str | None = None,
        source_type: str = "manual",
    ) -> None:
        self.address = address
        self.channel_hint = channel_hint
        self.display_name = display_name
        self.external_ref = external_ref
        self.source_type = source_type


class ContactService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.contacts = ManualContactRepository(session)
        self.sources = ContactSourceRepository(session)

    async def create_manual(
        self, tenant_id: UUID, data: dict[str, Any], *, actor_id: str | None = None
    ) -> ManualContact:
        validate_contact(data)
        entity = ManualContact(
            tenant_id=tenant_id,
            display_name=data.get("display_name"),
            phone=data.get("phone"),
            email=data.get("email"),
            push_token=data.get("push_token"),
            metadata_json=data.get("metadata_json"),
            tags=data.get("tags"),
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.contacts.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_manual(self, tenant_id: UUID) -> list[ManualContact]:
        return list(await self.contacts.list_by_tenant(tenant_id, limit=500))

    async def import_csv(
        self, tenant_id: UUID, csv_text: str, *, actor_id: str | None = None
    ) -> list[ManualContact]:
        reader = csv.DictReader(io.StringIO(csv_text))
        created: list[ManualContact] = []
        for row in reader:
            payload = {
                "display_name": row.get("display_name") or row.get("name"),
                "phone": row.get("phone") or row.get("mobile"),
                "email": row.get("email"),
                "push_token": row.get("push_token"),
            }
            validate_contact(payload)
            entity = ManualContact(
                tenant_id=tenant_id,
                display_name=payload.get("display_name"),
                phone=payload.get("phone"),
                email=payload.get("email"),
                push_token=payload.get("push_token"),
                created_by=actor_id,
                updated_by=actor_id,
            )
            await self.contacts.add(entity)
            created.append(entity)
        await self.session.commit()
        return created

    async def create_source(
        self, tenant_id: UUID, data: dict[str, Any], *, actor_id: str | None = None
    ) -> ContactSource:
        validate_contact_source(data)
        entity = ContactSource(
            tenant_id=tenant_id,
            name=data["name"],
            source_type=data["source_type"],
            base_url=data.get("base_url"),
            endpoint_path=data.get("endpoint_path"),
            auth_config=data.get("auth_config"),
            query_mapping=data.get("query_mapping"),
            field_mapping=data.get("field_mapping"),
            is_active=data.get("is_active", True),
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.sources.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_sources(self, tenant_id: UUID) -> list[ContactSource]:
        return list(await self.sources.list_by_tenant(tenant_id))

    async def resolve(
        self,
        tenant_id: UUID,
        *,
        source_id: UUID | None = None,
        query: dict[str, Any] | None = None,
        channel: str = "sms",
    ) -> list[ResolvedContact]:
        """Resolve contacts dynamically — never persists remote business records."""
        query = query or {}
        if source_id is None:
            # Manual store resolution by phone/email filters
            manuals = await self.list_manual(tenant_id)
            results: list[ResolvedContact] = []
            for c in manuals:
                address = c.phone if channel == "sms" else (c.email or c.phone)
                if not address:
                    continue
                if query.get("phone") and c.phone != query["phone"]:
                    continue
                if query.get("email") and c.email != query["email"]:
                    continue
                results.append(
                    ResolvedContact(
                        address=address,
                        display_name=c.display_name,
                        source_type="manual",
                    )
                )
            return results

        source = await self.sources.get(tenant_id, source_id)
        if source is None or not source.is_active:
            raise NotFoundError("Contact source not found")

        if source.source_type in ("manual", "csv"):
            return await self.resolve(tenant_id, query=query, channel=channel)

        return await self._resolve_remote(source, query=query, channel=channel)

    async def _resolve_remote(
        self,
        source: ContactSource,
        *,
        query: dict[str, Any],
        channel: str,
    ) -> list[ResolvedContact]:
        if not source.base_url:
            raise AppError(
                "Contact source missing base_url",
                status_code=422,
                error_code="invalid_source",
            )
        path = source.endpoint_path or ""
        url = source.base_url.rstrip("/") + "/" + path.lstrip("/")
        headers: dict[str, str] = {}
        auth = source.auth_config or {}
        if auth.get("bearer"):
            headers["Authorization"] = f"Bearer {auth['bearer']}"
        if auth.get("api_key"):
            headers[auth.get("api_key_header", "X-API-Key")] = auth["api_key"]
        params = dict(source.query_mapping or {})
        params.update(query)

        # In tests, callers can inject a stub via settings-like auth_config mock_contacts
        if auth.get("mock_contacts") is not None:
            rows = auth["mock_contacts"]
        else:
            try:
                async with httpx.AsyncClient(timeout=10.0) as client:
                    response = await client.get(url, params=params, headers=headers)
                    response.raise_for_status()
                    payload = response.json()
            except Exception as exc:  # noqa: BLE001
                raise AppError(
                    f"Failed to resolve contacts from {source.source_type}",
                    status_code=502,
                    error_code="contact_source_error",
                    details={"error": str(exc)},
                ) from exc
            rows = payload if isinstance(payload, list) else payload.get("items", [])

        mapping = source.field_mapping or {}
        phone_field = mapping.get("phone", "phone")
        email_field = mapping.get("email", "email")
        name_field = mapping.get("display_name", "display_name")
        id_field = mapping.get("id", "id")
        results: list[ResolvedContact] = []
        for row in rows:
            address = row.get(phone_field) if channel == "sms" else row.get(email_field)
            if channel == "sms" and not address:
                address = row.get(phone_field)
            if not address:
                continue
            results.append(
                ResolvedContact(
                    address=str(address),
                    display_name=str(row.get(name_field) or "") or None,
                    external_ref=str(row.get(id_field)) if row.get(id_field) else None,
                    source_type=source.source_type,
                )
            )
        return results
