"""
alloggiati/service.py
=====================
Core orchestration layer for Alloggiati Web sync.

Implements AlloggiatiService.sync() — the single entry point called by the
API view for POST /api/alloggiati/sync.

Pipeline (8 steps):
  1. Validate input (structure exists, credential exists, date range valid)
  2. Fetch bookings in range (confirmed / checked-in, deduplicated)
  3. Transform guests via existing transformer
  4. Authenticate with Alloggiati Web (hard stop on failure)
  5. Send valid guest payloads via AlloggiatiClient
  6. Parse and classify the server response
  7. Persist AlloggiatiSyncLog (always — even on error)
  8. Return structured response to the view

Security rules:
  - Credentials are decrypted in memory only, never logged or re-persisted
  - No guest PII (names, document numbers) appears in logs or responses
  - AlloggiatiSyncLog stores only aggregated counts

GDPR compliance:
  - Only metadata (counts, status, date range) is persisted
"""

from __future__ import annotations

import datetime
import logging
from typing import Any, Dict, List, Optional, Tuple

from django.db import transaction
from django.db.models import Q
from django.utils import timezone

from alloggiati.client import (
    AlloggiatiAuthError,
    AlloggiatiClient,
    AlloggiatiClientError,
    AlloggiatiNetworkError,
    AlloggiatiResponseError,
)
from alloggiati.models import AlloggiatiCredential, AlloggiatiSyncLog
from alloggiati.transformer import transform_booking_guests
from bookings.models import Booking
from structures.models import Structure

logger = logging.getLogger("alloggiati")


# ---------------------------------------------------------------------------
# Response status constants (mirror AlloggiatiSyncLog.Status)
# ---------------------------------------------------------------------------

STATUS_CONNECTED = AlloggiatiSyncLog.Status.CONNECTED
STATUS_PARTIAL = AlloggiatiSyncLog.Status.PARTIAL
STATUS_ERROR = AlloggiatiSyncLog.Status.ERROR


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _parse_date(value: str, field_name: str) -> datetime.date:
    """
    Parse a YYYY-MM-DD string into a datetime.date.

    Raises:
        ValueError: With a human-readable message on failure.
    """
    try:
        return datetime.date.fromisoformat(value.strip())
    except (ValueError, AttributeError):
        raise ValueError(f"{field_name} must be a valid date in YYYY-MM-DD format.")


def _validate_date_range(date_from: datetime.date, date_to: datetime.date) -> None:
    """
    Ensure date_from <= date_to and the range is not unreasonably large.

    Raises:
        ValueError: On invalid range.
    """
    if date_from > date_to:
        raise ValueError("date_from cannot be later than date_to.")
    if (date_to - date_from).days > 366:
        raise ValueError("Sync date range cannot exceed 366 days.")


def _fetch_bookings(
    structure: Structure,
    date_from: datetime.date,
    date_to: datetime.date,
) -> List[Booking]:
    """
    Fetch bookings for the structure that overlap the given date range.

    Overlap condition: check_in_date < date_to AND check_out_date > date_from

    Filters:
      - is_checked_in=True  (confirmed / checked-in guests only)
      - Deduplication by pk (distinct)

    Returns:
        List of Booking instances with guests and structure prefetched.
    """
    return list(
        Booking.objects.filter(
            structure=structure,
            is_checked_in=True,
            check_in_date__range=[date_from, date_to],
        )
        .select_related("structure")
        .prefetch_related("guests")
        .distinct()
        .order_by("check_in_date", "id")
    )   


def _build_client(credential: AlloggiatiCredential) -> AlloggiatiClient:
    """
    Instantiate an AlloggiatiClient from a credential record.

    Credentials are decrypted in memory here and passed directly to the
    client constructor — they are never stored in variables that outlive
    this function's scope beyond the client object itself.

    Raises:
        AlloggiatiAuthError: If required credential fields are missing.
    """
    if credential.mode == AlloggiatiCredential.MODE_DIGITAL_CERTIFICATE:
        cert = credential.certificate
        key = credential.private_key
        if not cert or not key:
            raise AlloggiatiAuthError(
                "Digital certificate or private key is missing for this structure."
            )
        return AlloggiatiClient.from_certificate(
            certificate_pem=cert,
            private_key_pem=key,
        )

    # CODES mode
    username = credential.username
    password = credential.password
    if not username or not password:
        raise AlloggiatiAuthError(
            "Alloggiati Web username or password is missing for this structure."
        )
    return AlloggiatiClient.from_codes(username=username, password=password)


def _persist_sync_log(
    *,
    structure: Structure,
    date_from: datetime.date,
    date_to: datetime.date,
    started_at: datetime.datetime,
    status: str,
    message: str,
    guests_sent: int,
    guests_rejected: int,
    validation_errors: Optional[List[Dict[str, Any]]] = None,
) -> AlloggiatiSyncLog:
    """
    Create and save an AlloggiatiSyncLog record.

    Always called — even on error — to maintain a complete audit trail.
    No PII (document numbers, credentials) is stored.
    validation_errors contains structured per-guest field errors only.
    """
    return AlloggiatiSyncLog.objects.create(
        structure=structure,
        date_from=date_from,
        date_to=date_to,
        started_at=started_at,
        completed_at=timezone.now(),
        status=status,
        message=message,
        guests_sent=guests_sent,
        guests_rejected=guests_rejected,
        validation_errors=validation_errors or [],
    )


def _build_response(
    *,
    status: str,
    sent: int,
    rejected: int,
    message: str,
    sync_log_id: Optional[str],
    validation_errors: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
    """Build the standardised API response dict."""
    return {
        "status": status,
        "sent": sent,
        "rejected": rejected,
        "message": message,
        "sync_log_id": str(sync_log_id) if sync_log_id else None,
        "validation_errors": validation_errors or [],
    }


# ---------------------------------------------------------------------------
# AlloggiatiService
# ---------------------------------------------------------------------------

class AlloggiatiService:
    """
    Core orchestration service for Alloggiati Web sync.

    All public methods are static — no instance state is required.
    """

    @staticmethod
    def sync(
        structure_id: Any,
        date_from: str,
        date_to: str,
    ) -> Dict[str, Any]:
        """
        Execute a full Alloggiati Web sync for a structure and date range.

        Args:
            structure_id: PK of the Structure (int or str).
            date_from:    Start of sync window, "YYYY-MM-DD".
            date_to:      End of sync window, "YYYY-MM-DD".

        Returns:
            {
                "status":            "CONNECTED" | "PARTIAL" | "ERROR",
                "sent":              int,
                "rejected":          int,
                "message":           str,
                "sync_log_id":       str | None,
                "validation_errors": [
                    {
                        "guest_id":   int,
                        "booking_id": int,
                        "guest_name": str,
                        "errors":     [{"field": str, "message": str}, ...]
                    },
                    ...
                ]
            }
        """
        started_at = timezone.now()

        # ----------------------------------------------------------------
        # STEP 1 — Validate input
        # ----------------------------------------------------------------
        try:
            structure = AlloggiatiService._get_structure(structure_id)
        except ValueError as exc:
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=0,
                message=str(exc),
                sync_log_id=None,
            )

        try:
            d_from = _parse_date(date_from, "date_from")
            d_to = _parse_date(date_to, "date_to")
            _validate_date_range(d_from, d_to)
        except ValueError as exc:
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=0,
                message=str(exc),
                sync_log_id=None,
            )

        try:
            credential = AlloggiatiService._get_credential(structure)
        except ValueError as exc:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message=str(exc),
                guests_sent=0,
                guests_rejected=0,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=0,
                message=str(exc),
                sync_log_id=log.id,
            )

        # ----------------------------------------------------------------
        # STEP 2 — Fetch bookings
        # ----------------------------------------------------------------
        bookings = _fetch_bookings(structure, d_from, d_to)

        if not bookings:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_PARTIAL,
                message="No checked-in bookings found in the selected date range.",
                guests_sent=0,
                guests_rejected=0,
            )
            return _build_response(
                status=STATUS_PARTIAL,
                sent=0,
                rejected=0,
                message="No checked-in bookings found in the selected date range.",
                sync_log_id=log.id,
            )

        # ----------------------------------------------------------------
        # STEP 3 — Transform guests
        # ----------------------------------------------------------------
        valid_payloads, invalid_records = AlloggiatiService._transform_all(bookings)

        transformer_rejected = len(invalid_records)

        # Build the serialisable validation_errors list from invalid_records.
        # Strip the internal "reason" key — only structured {field, message}
        # errors are exposed to callers.  guest_name is included for display
        # but contains no document numbers or credentials.
        serialisable_errors: List[Dict[str, Any]] = [
            {
                "guest_id":   rec.get("guest_id"),
                "booking_id": rec.get("booking_id"),
                "guest_name": rec.get("guest_name", "Unknown Guest"),
                "errors":     rec.get("errors", []),
            }
            for rec in invalid_records
        ]

        if not valid_payloads:
            n = transformer_rejected
            message = (
                f"{n} guest(s) failed validation and cannot be submitted "
                "to Alloggiati Web."
            )
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_PARTIAL,
                message=message,
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_PARTIAL,
                sent=0,
                rejected=transformer_rejected,
                message=message,
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )

        # ----------------------------------------------------------------
        # STEP 4 — Authenticate (hard stop on failure)
        # ----------------------------------------------------------------
        try:
            client = _build_client(credential)
        except AlloggiatiAuthError:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message="Authentication failed: invalid or missing credentials.",
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=transformer_rejected,
                message="Authentication failed: invalid or missing credentials.",
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )

        # ----------------------------------------------------------------
        # STEP 5 — Send data to Alloggiati Web
        # ----------------------------------------------------------------
        try:
            send_result = client.send_guests(valid_payloads)
        except AlloggiatiAuthError:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message="Authentication failed during send.",
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=transformer_rejected,
                message="Authentication failed during send.",
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )
        except AlloggiatiNetworkError as exc:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message=f"Network error: {exc}",
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=transformer_rejected,
                message=f"Network error: {exc}",
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )
        except AlloggiatiResponseError as exc:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message=f"Server response error: {exc}",
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=transformer_rejected,
                message=f"Server response error: {exc}",
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )
        except AlloggiatiClientError as exc:
            log = _persist_sync_log(
                structure=structure,
                date_from=d_from,
                date_to=d_to,
                started_at=started_at,
                status=STATUS_ERROR,
                message=f"Alloggiati client error: {exc}",
                guests_sent=0,
                guests_rejected=transformer_rejected,
                validation_errors=serialisable_errors,
            )
            return _build_response(
                status=STATUS_ERROR,
                sent=0,
                rejected=transformer_rejected,
                message=f"Alloggiati client error: {exc}",
                sync_log_id=log.id,
                validation_errors=serialisable_errors,
            )

        # ----------------------------------------------------------------
        # STEP 6 — Parse response and classify outcome
        # ----------------------------------------------------------------
        server_accepted = send_result.get("accepted", 0)
        server_rejected = send_result.get("rejected", 0)
        total_rejected = transformer_rejected + server_rejected

        if send_result.get("success") and server_rejected == 0 and transformer_rejected == 0:
            final_status = STATUS_CONNECTED
            message = (
                f"Sync completed successfully. "
                f"{server_accepted} guest(s) accepted by Alloggiati Web."
            )
        elif send_result.get("success") and total_rejected > 0:
            final_status = STATUS_PARTIAL
            message = (
                f"Partial sync: {server_accepted} guest(s) accepted, "
                f"{total_rejected} rejected "
                f"({transformer_rejected} failed validation, "
                f"{server_rejected} rejected by server)."
            )
        else:
            final_status = STATUS_ERROR
            raw_msg = send_result.get("raw_message") or "Unknown server error."
            message = f"Sync failed. Server message: {raw_msg}"

        # ----------------------------------------------------------------
        # STEP 7 — Persist sync log
        # ----------------------------------------------------------------
        log = _persist_sync_log(
            structure=structure,
            date_from=d_from,
            date_to=d_to,
            started_at=started_at,
            status=final_status,
            message=message,
            guests_sent=server_accepted,
            guests_rejected=total_rejected,
            validation_errors=serialisable_errors,
        )

        # ----------------------------------------------------------------
        # STEP 8 — Return response
        # ----------------------------------------------------------------
        return _build_response(
            status=final_status,
            sent=server_accepted,
            rejected=total_rejected,
            message=message,
            sync_log_id=log.id,
            validation_errors=serialisable_errors,
        )

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _get_structure(structure_id: Any) -> Structure:
        """
        Fetch a Structure by PK.

        Raises:
            ValueError: If not found or ID is invalid.
        """
        try:
            pk = int(structure_id)
        except (TypeError, ValueError):
            raise ValueError(f"Invalid structure_id: {structure_id!r}.")

        try:
            return Structure.objects.get(pk=pk)
        except Structure.DoesNotExist:
            raise ValueError(f"Structure with id={pk} does not exist.")

    @staticmethod
    def _get_credential(structure: Structure) -> AlloggiatiCredential:
        """
        Fetch the AlloggiatiCredential for a structure.

        Raises:
            ValueError: If no credential is configured.
        """
        try:
            return AlloggiatiCredential.objects.get(structure=structure)
        except AlloggiatiCredential.DoesNotExist:
            raise ValueError(
                f"No Alloggiati Web credentials configured for structure "
                f"'{structure.name}'. Please add credentials before syncing."
            )

    @staticmethod
    def _transform_all(
        bookings: List[Booking],
    ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
        """
        Run transform_booking_guests on every booking and aggregate results.

        Deduplicates valid payloads by (document_number, arrival_date) to
        prevent double-submission on retry.

        Returns:
            (valid_payloads, invalid_records)
        """
        all_valid: List[Dict[str, Any]] = []
        all_invalid: List[Dict[str, Any]] = []
        seen: set = set()

        for booking in bookings:
            result = transform_booking_guests(booking)
            for payload in result.get("valid", []):
                # Idempotency key: document number + arrival date
                dedup_key = (
                    payload.get("document_number", ""),
                    payload.get("arrival_date", ""),
                )
                if dedup_key in seen:
                    continue
                seen.add(dedup_key)
                all_valid.append(payload)

            all_invalid.extend(result.get("invalid", []))

        return all_valid, all_invalid
