"""DHL shipment status-change notification service.

When a DHL tracking status changes (``picked_up``, ``in_transit``,
``out_for_delivery``, ``delivered``, etc.), send the customer an email with
the latest event and a link to ``/shop/track/:number``. Every attempt —
successful or not — is logged to the ``shipment_notifications`` table.

The service is intentionally idempotent: calling ``notify_if_status_changed``
multiple times for a shipment that is still in the same status is a no-op,
so admins can safely trigger a "Check now" action without spamming the
customer.

The module is framework-agnostic; the FastAPI routers simply inject the
SQLAlchemy session and the ``Orders`` row.
"""

from __future__ import annotations

import logging
import os
from datetime import datetime, timezone
from html import escape
from typing import Any, Dict, List, Optional, Sequence

from models.orders import Orders
from models.shipment_notifications import ShipmentNotification
from services.dhl.client import DhlSettings, get_tracking
from services.email_sender import send_email
from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)


# Terminal statuses we consider "already final" — once we've sent a
# ``delivered`` notification we don't want to send anything else even if the
# DHL API keeps returning events.
_TERMINAL_STATUSES = {"delivered"}


# Friendly title + one-line preamble for each canonical status. Anything we
# don't know about falls back to a generic "Shipment update" message.
_STATUS_COPY: Dict[str, Dict[str, str]] = {
    "created": {
        "title": "Your shipment has been created",
        "intro": "Your order is being prepared for pickup by DHL.",
    },
    "picked_up": {
        "title": "Your shipment has been picked up",
        "intro": "DHL has collected your package and it's now on its way.",
    },
    "in_transit": {
        "title": "Your shipment is in transit",
        "intro": "Your package is moving through the DHL network.",
    },
    "out_for_delivery": {
        "title": "Your shipment is out for delivery",
        "intro": "DHL is delivering your package today.",
    },
    "delivered": {
        "title": "Your shipment has been delivered",
        "intro": "Your package has been delivered. Thank you for your order!",
    },
    "exception": {
        "title": "There's an update on your shipment",
        "intro": "DHL reported an exception for your shipment. Please review the details below.",
    },
}


def _normalize_status(raw: Optional[str]) -> str:
    """Map a DHL status string to our canonical, lowercase-underscored form."""
    if not raw:
        return "in_transit"
    s = str(raw).strip().lower().replace("-", "_").replace(" ", "_")
    # Common DHL vocabulary mappings.
    aliases = {
        "transit": "in_transit",
        "intransit": "in_transit",
        "pickup": "picked_up",
        "pickedup": "picked_up",
        "collected": "picked_up",
        "ofd": "out_for_delivery",
        "withdeliverycourier": "out_for_delivery",
        "success": "delivered",
        "ok": "delivered",
    }
    return aliases.get(s, s)


def _public_site_base_url() -> str:
    """Resolve the public base URL used to build tracking links.

    Order of precedence:
    1. ``PUBLIC_SITE_URL`` env var (set per deployment).
    2. ``FRONTEND_BASE_URL`` env var (fallback used elsewhere in the code).
    3. Empty string — the email then renders a relative link, which is
       still usable once the customer opens it in a web browser.
    """
    for name in ("PUBLIC_SITE_URL", "FRONTEND_BASE_URL"):
        value = os.environ.get(name)
        if value:
            return value.rstrip("/")
    return ""


def _build_tracking_url(tracking_number: str) -> str:
    base = _public_site_base_url()
    path = f"/shop/track/{tracking_number}"
    return f"{base}{path}" if base else path


def _pick_latest_event(events: Sequence[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    """Return the most informative event from the DHL response.

    The DHL API returns events in reverse chronological order in most cases
    but isn't guaranteed to. We keep the first event that has a description
    (which is what the customer sees in the email), otherwise we fall back
    to the last entry.
    """
    if not events:
        return None
    for evt in events:
        if evt.get("description"):
            return evt
    return events[-1]


def _render_email(
    *,
    tracking_number: str,
    status: str,
    order_number: Optional[str],
    event: Optional[Dict[str, Any]],
    tracking_url: str,
) -> Dict[str, str]:
    """Render plain-text + HTML bodies for the notification email."""

    copy = _STATUS_COPY.get(status, {
        "title": "Shipment update",
        "intro": "There's a new update on your shipment.",
    })

    event_desc = (event or {}).get("description") or "Status updated"
    event_loc = (event or {}).get("location") or ""
    event_ts = (event or {}).get("timestamp") or ""

    order_ref = f" for order {order_number}" if order_number else ""

    text_lines = [
        f"{copy['title']}{order_ref}",
        "",
        copy["intro"],
        "",
        f"Tracking number: {tracking_number}",
        f"Current status:  {status.replace('_', ' ').title()}",
        f"Latest event:    {event_desc}",
    ]
    if event_loc:
        text_lines.append(f"Location:        {event_loc}")
    if event_ts:
        text_lines.append(f"Time:            {event_ts}")
    text_lines += [
        "",
        f"Track your shipment: {tracking_url}",
        "",
        "Thank you for shopping with us.",
    ]
    text_body = "\n".join(text_lines)

    safe_tracking = escape(tracking_number)
    safe_title = escape(copy["title"])
    safe_intro = escape(copy["intro"])
    safe_status = escape(status.replace("_", " ").title())
    safe_event_desc = escape(event_desc)
    safe_event_loc = escape(event_loc)
    safe_event_ts = escape(event_ts)
    safe_order = escape(order_ref)
    safe_url = escape(tracking_url)

    event_rows = [
        f"<tr><td style='padding:4px 12px;color:#6b7280'>Tracking</td>"
        f"<td style='padding:4px 12px;font-family:monospace'>{safe_tracking}</td></tr>",
        f"<tr><td style='padding:4px 12px;color:#6b7280'>Status</td>"
        f"<td style='padding:4px 12px'><strong>{safe_status}</strong></td></tr>",
        f"<tr><td style='padding:4px 12px;color:#6b7280'>Latest event</td>"
        f"<td style='padding:4px 12px'>{safe_event_desc}</td></tr>",
    ]
    if event_loc:
        event_rows.append(
            f"<tr><td style='padding:4px 12px;color:#6b7280'>Location</td>"
            f"<td style='padding:4px 12px'>{safe_event_loc}</td></tr>"
        )
    if event_ts:
        event_rows.append(
            f"<tr><td style='padding:4px 12px;color:#6b7280'>Time</td>"
            f"<td style='padding:4px 12px'>{safe_event_ts}</td></tr>"
        )

    html_body = f"""\
<!doctype html>
<html>
  <body style="margin:0;padding:0;background:#f9fafb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111827">
    <table width="100%" cellspacing="0" cellpadding="0" style="background:#f9fafb;padding:24px 0">
      <tr><td align="center">
        <table width="560" cellspacing="0" cellpadding="0" style="background:#ffffff;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,0.08);overflow:hidden">
          <tr>
            <td style="background:#111827;color:#ffffff;padding:20px 28px">
              <h1 style="margin:0;font-size:20px;font-weight:600">{safe_title}{safe_order}</h1>
            </td>
          </tr>
          <tr>
            <td style="padding:24px 28px;font-size:15px;line-height:1.6">
              <p style="margin:0 0 16px 0">{safe_intro}</p>
              <table cellspacing="0" cellpadding="0"
                     style="width:100%;border-collapse:collapse;background:#f3f4f6;border-radius:8px;margin-bottom:20px">
                {''.join(event_rows)}
              </table>
              <p style="margin:0 0 20px 0">
                <a href="{safe_url}"
                   style="display:inline-block;background:#2563eb;color:#ffffff;text-decoration:none;
                          padding:12px 22px;border-radius:8px;font-weight:600">
                  Track your shipment
                </a>
              </p>
              <p style="margin:0;color:#6b7280;font-size:13px">
                If the button above doesn't work, copy this link:<br>
                <span style="word-break:break-all">{safe_url}</span>
              </p>
            </td>
          </tr>
          <tr>
            <td style="padding:14px 28px;background:#f9fafb;color:#6b7280;font-size:12px">
              You are receiving this email because you placed an order with us.
            </td>
          </tr>
        </table>
      </td></tr>
    </table>
  </body>
</html>
"""

    return {"text": text_body, "html": html_body, "subject": f"{copy['title']}{order_ref}"}


async def _log_notification(
    db: AsyncSession,
    *,
    order: Optional[Orders],
    tracking_number: str,
    recipient_email: str,
    previous_status: Optional[str],
    new_status: str,
    event: Optional[Dict[str, Any]],
    success: bool,
    error_message: Optional[str],
    stub: bool,
) -> ShipmentNotification:
    row = ShipmentNotification(
        order_id=order.id if order else None,
        order_number=order.order_number if order else None,
        tracking_number=tracking_number,
        recipient_email=recipient_email,
        previous_status=previous_status,
        new_status=new_status,
        event_description=(event or {}).get("description"),
        event_location=(event or {}).get("location"),
        event_timestamp=str((event or {}).get("timestamp") or "") or None,
        success=success,
        error_message=error_message,
        stub=stub,
    )
    db.add(row)
    try:
        await db.commit()
        await db.refresh(row)
    except Exception:  # noqa: BLE001
        logger.exception("[dhl.notifications] Failed to persist notification row")
        await db.rollback()
    return row


async def notify_if_status_changed(
    db: AsyncSession,
    *,
    order: Orders,
    settings: DhlSettings,
    force: bool = False,
) -> Dict[str, Any]:
    """Fetch tracking, compare to ``order.last_notified_status`` and email on change.

    Returns a small dict describing the outcome — useful for the admin
    "Check now" endpoint so the UI can show a toast.

    The function is safe to call even when:
      - The order has no tracking number yet (returns ``skipped=no_tracking``).
      - The customer has no billing email (returns ``skipped=no_recipient``).
      - The DHL status hasn't changed since the last notification
        (returns ``skipped=unchanged``, unless ``force=True``).
    """

    tracking_number = (order.tracking_number or "").strip()
    if not tracking_number:
        return {"sent": False, "reason": "no_tracking"}

    recipient = (order.billing_email or "").strip()
    if not recipient:
        return {"sent": False, "reason": "no_recipient"}

    reference_time = order.updated_at or order.created_at
    if reference_time is not None and reference_time.tzinfo is None:
        reference_time = reference_time.replace(tzinfo=timezone.utc)

    tracking = await get_tracking(settings, tracking_number, created_at=reference_time)
    raw_status = tracking.get("status") or "in_transit"
    new_status = _normalize_status(raw_status)
    previous_status = order.last_notified_status

    if previous_status in _TERMINAL_STATUSES and not force:
        return {"sent": False, "reason": "already_terminal", "status": previous_status}

    if new_status == previous_status and not force:
        return {
            "sent": False,
            "reason": "unchanged",
            "status": new_status,
            "tracking_number": tracking_number,
        }

    events: List[Dict[str, Any]] = tracking.get("events") or []
    latest_event = _pick_latest_event(events)
    tracking_url = _build_tracking_url(tracking_number)
    rendered = _render_email(
        tracking_number=tracking_number,
        status=new_status,
        order_number=order.order_number,
        event=latest_event,
        tracking_url=tracking_url,
    )

    success = False
    error_message: Optional[str] = None
    try:
        await send_email(
            to_addrs=[recipient],
            subject=rendered["subject"],
            text_body=rendered["text"],
            html_body=rendered["html"],
        )
        success = True
    except Exception as exc:  # noqa: BLE001
        # Don't raise: we still want to log the attempt so admins can see
        # the failure in the delivery log.
        error_message = f"{type(exc).__name__}: {exc}"
        logger.warning(
            "[dhl.notifications] Failed to send notification for %s: %s",
            tracking_number, error_message,
        )

    # Only advance ``last_notified_status`` on success, so a failed send is
    # retried next time ``check-tracking`` is called.
    if success:
        order.last_notified_status = new_status
        try:
            await db.commit()
        except Exception:  # noqa: BLE001
            logger.exception("[dhl.notifications] Failed to update order.last_notified_status")
            await db.rollback()

    log_row = await _log_notification(
        db,
        order=order,
        tracking_number=tracking_number,
        recipient_email=recipient,
        previous_status=previous_status,
        new_status=new_status,
        event=latest_event,
        success=success,
        error_message=error_message,
        stub=bool(tracking.get("stub")),
    )

    return {
        "sent": success,
        "reason": "sent" if success else "send_failed",
        "status": new_status,
        "previous_status": previous_status,
        "tracking_number": tracking_number,
        "recipient": recipient,
        "notification_id": log_row.id,
        "error": error_message,
        "stub": bool(tracking.get("stub")),
    }


async def list_notifications(
    db: AsyncSession,
    *,
    order_id: Optional[int] = None,
    tracking_number: Optional[str] = None,
    success: Optional[bool] = None,
    limit: int = 100,
) -> List[ShipmentNotification]:
    """Return notification log rows matching the given filters, newest first."""
    stmt = select(ShipmentNotification)
    if order_id is not None:
        stmt = stmt.where(ShipmentNotification.order_id == order_id)
    if tracking_number:
        stmt = stmt.where(ShipmentNotification.tracking_number == tracking_number.strip())
    if success is not None:
        stmt = stmt.where(ShipmentNotification.success == success)
    stmt = stmt.order_by(desc(ShipmentNotification.created_at)).limit(max(1, min(limit, 500)))
    result = await db.execute(stmt)
    return list(result.scalars().all())


__all__ = [
    "notify_if_status_changed",
    "list_notifications",
]