"""Unified contact service — CRUD, FTS search, contactpersons, soft-delete, export."""

from __future__ import annotations

import csv
import io
import uuid
from typing import Any

from sqlalchemy import select, func, or_, text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.models.contact import Contact, ContactPerson
from app.services.entity_history_service import record_history


def _compute_displayname(data: dict) -> str:
    """Compute displayname from type and name fields."""
    if data.get("type") == "person":
        parts = [data.get("surfix"), data.get("firstname"), data.get("surname")]
        return " ".join(p for p in parts if p).strip()
    else:
        return data.get("name") or ""


def _serialize_contact(c: Contact) -> dict:
    """Serialize a Contact ORM object to dict."""
    return {
        "id": str(c.id),
        "type": c.type,
        "displayname": c.displayname,
        "name": c.name,
        "firstname": c.firstname,
        "surname": c.surname,
        "surfix": c.surfix,
        "ext_name_line": c.ext_name_line,
        "gender": c.gender,
        "code": c.code,
        "accounting_code": c.accounting_code,
        "vendor_accounting_code": c.vendor_accounting_code,
        "mailing_street": c.mailing_street,
        "mailing_number": c.mailing_number,
        "mailing_unit_number": c.mailing_unit_number,
        "mailing_district": c.mailing_district,
        "mailing_extra_address_line": c.mailing_extra_address_line,
        "mailing_postalcode": c.mailing_postalcode,
        "mailing_city": c.mailing_city,
        "mailing_state": c.mailing_state,
        "mailing_country": c.mailing_country,
        "visit_street": c.visit_street,
        "visit_number": c.visit_number,
        "visit_unit_number": c.visit_unit_number,
        "visit_district": c.visit_district,
        "visit_extra_address_line": c.visit_extra_address_line,
        "visit_postalcode": c.visit_postalcode,
        "visit_city": c.visit_city,
        "visit_state": c.visit_state,
        "invoice_street": c.invoice_street,
        "invoice_number": c.invoice_number,
        "invoice_unit_number": c.invoice_unit_number,
        "invoice_district": c.invoice_district,
        "invoice_extra_address_line": c.invoice_extra_address_line,
        "invoice_postalcode": c.invoice_postalcode,
        "invoice_city": c.invoice_city,
        "invoice_state": c.invoice_state,
        "invoice_country": c.invoice_country,
        "country": c.country,
        "phone_1": c.phone_1,
        "phone_2": c.phone_2,
        "email_1": c.email_1,
        "email_2": c.email_2,
        "website": c.website,
        "vat_code": c.vat_code,
        "fiscal_code": c.fiscal_code,
        "commerce_code": c.commerce_code,
        "purchase_number": c.purchase_number,
        "bic": c.bic,
        "bank_account": c.bank_account,
        "discount_crew": c.discount_crew,
        "discount_transport": c.discount_transport,
        "discount_rental": c.discount_rental,
        "discount_sale": c.discount_sale,
        "discount_subrent": c.discount_subrent,
        "discount_total": c.discount_total,
        "latitude": c.latitude,
        "longitude": c.longitude,
        "projectnote": c.projectnote,
        "projectnote_title": c.projectnote_title,
        "contact_warning": c.contact_warning,
        "tags": c.tags,
        "image": c.image,
        "custom": c.custom,
        "folder_id": str(c.folder_id) if c.folder_id else None,
        "default_person_id": str(c.default_person_id) if c.default_person_id else None,
        "admin_contactperson_id": str(c.admin_contactperson_id) if c.admin_contactperson_id else None,
        "created_at": c.created_at.isoformat() if c.created_at else None,
        "updated_at": c.updated_at.isoformat() if c.updated_at else None,
    }


def _serialize_contact_person(cp: ContactPerson) -> dict:
    """Serialize a ContactPerson ORM object to dict."""
    return {
        "id": str(cp.id),
        "contact_id": str(cp.contact_id),
        "displayname": cp.displayname,
        "firstname": cp.firstname,
        "middle_name": cp.middle_name,
        "lastname": cp.lastname,
        "function": cp.function,
        "phone": cp.phone,
        "mobilephone": cp.mobilephone,
        "email": cp.email,
        "street": cp.street,
        "number": cp.number,
        "postalcode": cp.postalcode,
        "city": cp.city,
        "state": cp.state,
        "country": cp.country,
        "tags": cp.tags,
        "custom": cp.custom,
        "created_at": cp.created_at.isoformat() if cp.created_at else None,
        "updated_at": cp.updated_at.isoformat() if cp.updated_at else None,
    }


def _serialize_contact_detail(c: Contact) -> dict:
    """Serialize with contact_persons included."""
    data = _serialize_contact(c)
    data["contact_persons"] = [_serialize_contact_person(cp) for cp in (c.contact_persons or [])]
    return data


async def list_contacts(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    page: int = 1,
    page_size: int = 20,
    search: str | None = None,
    contact_type: str | None = None,
    folder_id: str | None = None,
    sort_by: str = "displayname",
    sort_order: str = "asc",
    resolved_perms: dict | None = None,
) -> dict:
    """List contacts with pagination, FTS search, type/folder filter, sorting."""
    base = select(Contact).where(
        Contact.tenant_id == tenant_id,
        Contact.deleted_at.is_(None),
    )

    if contact_type:
        base = base.where(Contact.type == contact_type)

    if folder_id:
        base = base.where(Contact.folder_id == uuid.UUID(folder_id))

    if search:
        base = base.where(
            Contact.search_tsv.op("@@")(func.plainto_tsquery("german", search))
        )

    # Count
    count_q = select(func.count()).select_from(base.subquery())
    total = (await db.execute(count_q)).scalar() or 0

    # Sort
    sort_col = getattr(Contact, sort_by, Contact.displayname)
    if sort_order == "desc":
        sort_col = sort_col.desc()
    base = base.order_by(sort_col)

    # Paginate
    offset = (page - 1) * page_size
    base = base.offset(offset).limit(page_size)

    result = await db.execute(base)
    contacts = result.scalars().all()

    return {
        "items": [_serialize_contact(c) for c in contacts],
        "total": total,
        "page": page,
        "page_size": page_size,
    }


async def get_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> dict:
    """Get a single contact with contact_persons."""
    q = (
        select(Contact)
        .options(selectinload(Contact.contact_persons))
        .where(
            Contact.id == uuid.UUID(contact_id),
            Contact.tenant_id == tenant_id,
            Contact.deleted_at.is_(None),
        )
    )
    result = await db.execute(q)
    contact = result.scalar_one_or_none()
    if not contact:
        raise ValueError("Contact not found")
    return _serialize_contact_detail(contact)


async def create_contact(
    db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, data: dict
) -> dict:
    """Create a new contact."""
    data["displayname"] = _compute_displayname(data)
    contact_persons_data = data.pop("contact_persons", None)

    contact = Contact(
        tenant_id=tenant_id,
        created_by=user_id,
        updated_by=user_id,
        **{k: v for k, v in data.items() if hasattr(Contact, k)},
    )
    db.add(contact)
    await db.flush()

    # Create inline contact persons
    if contact_persons_data:
        for cp_data in contact_persons_data:
            cp_data["displayname"] = " ".join(
                p for p in [cp_data.get("firstname"), cp_data.get("lastname")] if p
            ).strip()
            cp = ContactPerson(
                tenant_id=tenant_id,
                contact_id=contact.id,
                created_by=user_id,
                updated_by=user_id,
                **{k: v for k, v in cp_data.items() if hasattr(ContactPerson, k)},
            )
            db.add(cp)
        await db.flush()

    # Load with contact_persons
    q = select(Contact).options(selectinload(Contact.contact_persons)).where(Contact.id == contact.id)
    result = await db.execute(q)
    contact = result.scalar_one()
    serialized = _serialize_contact_detail(contact)

    # Record history
    await record_history(
        db, tenant_id, user_id, "contact", contact.id,
        action="create", snapshot_after=serialized,
    )

    return serialized


async def update_contact(
    db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, contact_id: str, data: dict
) -> dict:
    """Update a contact."""
    q = (
        select(Contact)
        .options(selectinload(Contact.contact_persons))
        .where(
            Contact.id == uuid.UUID(contact_id),
            Contact.tenant_id == tenant_id,
            Contact.deleted_at.is_(None),
        )
    )
    result = await db.execute(q)
    contact = result.scalar_one_or_none()
    if not contact:
        raise ValueError("Contact not found")

    # Capture snapshot before update
    snapshot_before = _serialize_contact_detail(contact)

    # Recompute displayname if name fields changed
    if any(k in data for k in ("type", "name", "firstname", "surname", "surfix")):
        merged = {**_serialize_contact(contact), **data}
        data["displayname"] = _compute_displayname(merged)

    for key, value in data.items():
        if hasattr(contact, key):
            setattr(contact, key, value)
    contact.updated_by = user_id

    await db.flush()
    snapshot_after = _serialize_contact_detail(contact)

    # Compute changes diff
    changes: dict = {}
    for key, new_val in snapshot_after.items():
        old_val = snapshot_before.get(key)
        if old_val != new_val:
            changes[key] = {"old": old_val, "new": new_val}

    # Record history
    await record_history(
        db, tenant_id, user_id, "contact", contact.id,
        action="update",
        snapshot_before=snapshot_before,
        snapshot_after=snapshot_after,
        changes=changes or None,
    )

    return snapshot_after


async def delete_contact(
    db: AsyncSession, tenant_id: uuid.UUID, contact_id: str, user_id: uuid.UUID | None = None
) -> None:
    """Soft-delete a contact."""
    q = select(Contact).where(
        Contact.id == uuid.UUID(contact_id),
        Contact.tenant_id == tenant_id,
        Contact.deleted_at.is_(None),
    )
    result = await db.execute(q)
    contact = result.scalar_one_or_none()
    if not contact:
        raise ValueError("Contact not found")

    # Capture snapshot before deletion
    from sqlalchemy.orm import selectinload
    q2 = (
        select(Contact)
        .options(selectinload(Contact.contact_persons))
        .where(Contact.id == contact.id)
    )
    result2 = await db.execute(q2)
    contact_full = result2.scalar_one()
    snapshot_before = _serialize_contact_detail(contact_full)

    from datetime import datetime, timezone
    contact.deleted_at = datetime.now(timezone.utc)
    await db.flush()

    # Record history
    await record_history(
        db, tenant_id, user_id, "contact", contact.id,
        action="delete", snapshot_before=snapshot_before,
    )


async def hard_delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> None:
    """GDPR hard-delete a contact."""
    q = select(Contact).where(
        Contact.id == uuid.UUID(contact_id),
        Contact.tenant_id == tenant_id,
    )
    result = await db.execute(q)
    contact = result.scalar_one_or_none()
    if not contact:
        raise ValueError("Contact not found")
    await db.delete(contact)
    await db.flush()


# ── ContactPerson CRUD ──

async def list_contact_persons(
    db: AsyncSession, tenant_id: uuid.UUID, contact_id: str
) -> list[dict]:
    """List all contact persons for a contact."""
    q = select(ContactPerson).where(
        ContactPerson.contact_id == uuid.UUID(contact_id),
        ContactPerson.tenant_id == tenant_id,
        ContactPerson.deleted_at.is_(None),
    ).order_by(ContactPerson.displayname)
    result = await db.execute(q)
    return [_serialize_contact_person(cp) for cp in result.scalars().all()]


async def create_contact_person(
    db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, contact_id: str, data: dict
) -> dict:
    """Add a contact person to a contact."""
    data["displayname"] = " ".join(
        p for p in [data.get("firstname"), data.get("lastname")] if p
    ).strip()
    cp = ContactPerson(
        tenant_id=tenant_id,
        contact_id=uuid.UUID(contact_id),
        created_by=user_id,
        updated_by=user_id,
        **{k: v for k, v in data.items() if hasattr(ContactPerson, k)},
    )
    db.add(cp)
    await db.flush()
    return _serialize_contact_person(cp)


async def update_contact_person(
    db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, contact_id: str, person_id: str, data: dict
) -> dict:
    """Update a contact person."""
    q = select(ContactPerson).where(
        ContactPerson.id == uuid.UUID(person_id),
        ContactPerson.contact_id == uuid.UUID(contact_id),
        ContactPerson.tenant_id == tenant_id,
        ContactPerson.deleted_at.is_(None),
    )
    result = await db.execute(q)
    cp = result.scalar_one_or_none()
    if not cp:
        raise ValueError("Contact person not found")

    if any(k in data for k in ("firstname", "lastname")):
        merged = {**_serialize_contact_person(cp), **data}
        data["displayname"] = " ".join(
            p for p in [merged.get("firstname"), merged.get("lastname")] if p
        ).strip()

    for key, value in data.items():
        if hasattr(cp, key):
            setattr(cp, key, value)
    cp.updated_by = user_id
    await db.flush()
    return _serialize_contact_person(cp)


async def delete_contact_person(
    db: AsyncSession, tenant_id: uuid.UUID, contact_id: str, person_id: str
) -> None:
    """Soft-delete a contact person."""
    q = select(ContactPerson).where(
        ContactPerson.id == uuid.UUID(person_id),
        ContactPerson.contact_id == uuid.UUID(contact_id),
        ContactPerson.tenant_id == tenant_id,
        ContactPerson.deleted_at.is_(None),
    )
    result = await db.execute(q)
    cp = result.scalar_one_or_none()
    if not cp:
        raise ValueError("Contact person not found")
    from datetime import datetime, timezone
    cp.deleted_at = datetime.now(timezone.utc)
    await db.flush()


async def export_contacts_csv(
    db: AsyncSession, tenant_id: uuid.UUID, contact_type: str | None = None, search: str | None = None
) -> str:
    """Export contacts as CSV string."""
    base = select(Contact).where(
        Contact.tenant_id == tenant_id,
        Contact.deleted_at.is_(None),
    )
    if contact_type:
        base = base.where(Contact.type == contact_type)
    if search:
        base = base.where(Contact.search_tsv.op("@@")(func.plainto_tsquery("german", search)))
    base = base.order_by(Contact.displayname)

    result = await db.execute(base)
    contacts = result.scalars().all()

    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow([
        "id", "type", "displayname", "name", "firstname", "surname", "code",
        "email_1", "email_2", "phone_1", "phone_2", "website",
        "mailing_city", "mailing_postalcode", "mailing_country",
        "vat_code", "tags",
    ])
    for c in contacts:
        writer.writerow([
            str(c.id), c.type, c.displayname, c.name or "", c.firstname or "", c.surname or "",
            c.code or "", c.email_1 or "", c.email_2 or "", c.phone_1 or "", c.phone_2 or "",
            c.website or "", c.mailing_city or "", c.mailing_postalcode or "",
            c.mailing_country or "", c.vat_code or "", c.tags or "",
        ])
    return output.getvalue()
