"""Low-level DHL client & credential helpers.

Credentials live in ``api_hub_credentials`` under ``provider_id = "dhl"``. We
store the three pieces admins configure from the CMS:

- ``api_key``        — DHL MyDHL API key (encrypted).
- ``api_secret``     — DHL MyDHL API secret / password (encrypted).
- ``account_number`` — DHL account number (shipper account).
- ``environment``    — ``"sandbox"`` or ``"production"``.

The ``api_hub_credentials`` table is a generic key/value store; we model
each field as its own row (``key`` column) to keep the schema simple and
consistent with how other providers use it in this codebase.

All outbound HTTP is wrapped in try/except and degrades to stub mode on any
error so a broken DHL environment never breaks order management for admins.
"""

from __future__ import annotations

import hashlib
import logging
import random
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional

import httpx
from core.mask_crypto import decrypt_text, encrypt_text
from models.api_hub_credentials import ApiHubCredential
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

DHL_PROVIDER_ID = "dhl"

# DHL MyDHL API base URLs (Express). We default to sandbox; the admin can
# switch to production from the settings page.
_DHL_BASE_URLS = {
    "sandbox": "https://express.api.dhl.com/mydhlapi/test",
    "production": "https://express.api.dhl.com/mydhlapi",
}

# Which keys belong to the DHL credential bundle.
_KEY_API_KEY = "api_key"
_KEY_API_SECRET = "api_secret"
_KEY_ACCOUNT = "account_number"
_KEY_ENVIRONMENT = "environment"
_ENCRYPTED_KEYS = {_KEY_API_KEY, _KEY_API_SECRET}


class DhlError(Exception):
    """Base class for DHL module errors."""


@dataclass
class DhlSettings:
    """In-memory representation of the DHL credential bundle."""

    api_key: Optional[str] = None
    api_secret: Optional[str] = None
    account_number: Optional[str] = None
    environment: str = "sandbox"

    @property
    def configured(self) -> bool:
        return bool(self.api_key and self.api_secret and self.account_number)

    def masked(self) -> Dict[str, Any]:
        """Return a safe representation for the admin UI (no plain secrets)."""

        def _mask(value: Optional[str]) -> Optional[str]:
            if not value:
                return None
            if len(value) <= 4:
                return "****"
            return value[:2] + "****" + value[-2:]

        return {
            "api_key": _mask(self.api_key),
            "api_secret": _mask(self.api_secret),
            "account_number": self.account_number or None,
            "environment": self.environment or "sandbox",
            "configured": self.configured,
        }


# ---------------------------------------------------------------------------
# Credential persistence
# ---------------------------------------------------------------------------


async def _load_rows(db: AsyncSession) -> Dict[str, ApiHubCredential]:
    result = await db.execute(
        select(ApiHubCredential).where(ApiHubCredential.provider_id == DHL_PROVIDER_ID)
    )
    return {row.name: row for row in result.scalars().all()}


async def get_settings(db: AsyncSession) -> DhlSettings:
    """Load DHL credentials and decrypt the encrypted fields.

    All values are stored encrypted via ``core.mask_crypto`` in the
    ``api_hub_credentials.encrypted_value`` column; non-secret fields
    (environment, account number) go through the same encryption so the
    schema stays uniform.
    """

    rows = await _load_rows(db)
    settings = DhlSettings()
    for key, row in rows.items():
        raw = row.encrypted_value or ""
        value = ""
        if raw:
            try:
                value = decrypt_text(raw)
            except Exception:  # noqa: BLE001
                logger.warning("[dhl] Failed to decrypt credential name=%s", key)
                value = ""
        if key == _KEY_API_KEY:
            settings.api_key = value or None
        elif key == _KEY_API_SECRET:
            settings.api_secret = value or None
        elif key == _KEY_ACCOUNT:
            settings.account_number = value or None
        elif key == _KEY_ENVIRONMENT:
            settings.environment = (value or "sandbox").strip().lower() or "sandbox"
    if settings.environment not in _DHL_BASE_URLS:
        settings.environment = "sandbox"
    return settings


async def save_settings(
    db: AsyncSession,
    *,
    api_key: Optional[str],
    api_secret: Optional[str],
    account_number: Optional[str],
    environment: str,
) -> DhlSettings:
    """Upsert the DHL credential bundle. ``None`` values are left unchanged.

    An empty string ``""`` explicitly clears the stored field (useful to
    rotate credentials).
    """

    environment = (environment or "sandbox").strip().lower()
    if environment not in _DHL_BASE_URLS:
        environment = "sandbox"

    rows = await _load_rows(db)

    pending: Dict[str, Optional[str]] = {
        _KEY_API_KEY: api_key,
        _KEY_API_SECRET: api_secret,
        _KEY_ACCOUNT: account_number,
        _KEY_ENVIRONMENT: environment,
    }

    for key, new_value in pending.items():
        if new_value is None:
            continue  # leave unchanged
        # Always encrypt before persisting so the table stays uniform even for
        # non-secret fields like the environment selector.
        stored_value = encrypt_text(new_value) if new_value else encrypt_text("")
        if key in rows:
            rows[key].encrypted_value = stored_value
        else:
            db.add(
                ApiHubCredential(
                    provider_id=DHL_PROVIDER_ID,
                    name=key,
                    encrypted_value=stored_value,
                )
            )

    await db.commit()
    return await get_settings(db)


async def delete_settings(db: AsyncSession) -> None:
    rows = await _load_rows(db)
    for row in rows.values():
        await db.delete(row)
    await db.commit()


# ---------------------------------------------------------------------------
# DHL API calls (with graceful stub fallback)
# ---------------------------------------------------------------------------


async def test_connection(settings: DhlSettings) -> Dict[str, Any]:
    """Verify that the configured DHL credentials can reach the API.

    Returns a result dict ``{"ok": bool, "message": str, "stub": bool}``.
    In stub mode (no credentials) we still return ``ok=True`` so admins can
    verify that the module itself is wired up.
    """

    if not settings.configured:
        return {
            "ok": True,
            "stub": True,
            "message": "DHL module is running in demo mode. Save credentials to enable live calls.",
        }

    base_url = _DHL_BASE_URLS[settings.environment]
    # A harmless GET against the "testConnection" endpoint for MyDHL API.
    url = f"{base_url}/address-validate"
    params = {"type": "delivery", "countryCode": "DE", "postalCode": "53113"}
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                url,
                params=params,
                auth=(settings.api_key or "", settings.api_secret or ""),
                headers={"Accept": "application/json"},
            )
        if resp.status_code in (200, 204):
            return {"ok": True, "stub": False, "message": "DHL connection OK."}
        if resp.status_code in (401, 403):
            return {
                "ok": False,
                "stub": False,
                "message": f"DHL rejected the credentials (HTTP {resp.status_code}).",
            }
        return {
            "ok": False,
            "stub": False,
            "message": f"DHL responded with HTTP {resp.status_code}.",
        }
    except httpx.HTTPError as exc:
        logger.warning("[dhl] test_connection network error: %s", exc)
        return {"ok": False, "stub": False, "message": f"Network error: {exc}"}


def _stub_tracking_number(seed: str) -> str:
    """Deterministic 10-digit DHL-ish tracking number derived from ``seed``.

    Using a hash keeps the number stable across retries for the same order.
    """

    digest = hashlib.sha1(seed.encode("utf-8")).hexdigest()
    numeric = int(digest, 16) % (10 ** 10)
    return f"DHL{numeric:010d}"


def _stub_events(tracking_number: str, created_at: datetime) -> List[Dict[str, Any]]:
    """Return a progressive, deterministic set of fake tracking events.

    Events advance with real time so the customer tracking page feels alive
    during demos: within the first few minutes the parcel is "picked up",
    after an hour it's "in transit", etc.
    """

    now = datetime.now(timezone.utc)
    age = now - created_at
    rng = random.Random(tracking_number)
    hub = rng.choice(["Leipzig Hub", "Cincinnati Hub", "East Midlands Hub", "Hong Kong Hub"])

    timeline = [
        (timedelta(minutes=0), "Shipment information received", "Origin"),
        (timedelta(minutes=15), "Picked up", "Origin"),
        (timedelta(hours=1), "Processed at origin facility", "Origin"),
        (timedelta(hours=4), f"Departed {hub}", hub),
        (timedelta(hours=12), "In transit", "International"),
        (timedelta(days=1), "Arrived at destination country", "Destination"),
        (timedelta(days=1, hours=6), "Out for delivery", "Destination"),
        (timedelta(days=1, hours=10), "Delivered", "Destination"),
    ]

    events: List[Dict[str, Any]] = []
    for offset, description, location in timeline:
        if age < offset:
            break
        ts = created_at + offset
        events.append(
            {
                "timestamp": ts.isoformat(),
                "description": description,
                "location": location,
            }
        )
    # Always show at least the first "information received" event.
    if not events:
        events.append(
            {
                "timestamp": created_at.isoformat(),
                "description": "Shipment information received",
                "location": "Origin",
            }
        )
    return events


async def create_shipment_for_order(
    settings: DhlSettings,
    *,
    order_number: str,
    recipient_name: str,
    recipient_address: Dict[str, Any],
    weight_kg: float = 1.0,
) -> Dict[str, Any]:
    """Create a DHL shipment for a shop order.

    Returns ``{"tracking_number": str, "label_url": str | None, "stub": bool}``.

    In stub mode we do NOT hit the network; we return a deterministic tracking
    number derived from ``order_number`` so repeated calls for the same order
    are idempotent from the UI's perspective.
    """

    if not settings.configured:
        tracking_number = _stub_tracking_number(f"{order_number}:{settings.environment}")
        return {
            "tracking_number": tracking_number,
            "label_url": None,
            "stub": True,
            "message": "Demo shipment created (DHL credentials not configured).",
        }

    base_url = _DHL_BASE_URLS[settings.environment]
    url = f"{base_url}/shipments"
    now = datetime.now(timezone.utc)
    payload = {
        "plannedShippingDateAndTime": now.strftime("%Y-%m-%dT%H:%M:%S GMT+00:00"),
        "pickup": {"isRequested": False},
        "productCode": "P",
        "accounts": [
            {"typeCode": "shipper", "number": settings.account_number},
        ],
        "customerDetails": {
            "shipperDetails": {
                "postalAddress": {
                    "cityName": "Bonn",
                    "countryCode": "DE",
                    "postalCode": "53113",
                    "addressLine1": "Charles-de-Gaulle-Strasse 20",
                },
                "contactInformation": {
                    "companyName": "DoItEasy",
                    "fullName": "DoItEasy Shipping",
                    "phone": "+49228182",
                    "email": "shipping@example.com",
                },
            },
            "receiverDetails": {
                "postalAddress": {
                    "cityName": recipient_address.get("city") or "Unknown",
                    "countryCode": (recipient_address.get("country") or "SA")[:2].upper(),
                    "postalCode": recipient_address.get("postal_code") or "00000",
                    "addressLine1": recipient_address.get("line1") or "Address line 1",
                },
                "contactInformation": {
                    "fullName": recipient_name or "Customer",
                    "phone": recipient_address.get("phone") or "0000000000",
                    "email": recipient_address.get("email") or "customer@example.com",
                    "companyName": recipient_name or "Customer",
                },
            },
        },
        "content": {
            "packages": [
                {
                    "weight": max(weight_kg, 0.1),
                    "dimensions": {"length": 20, "width": 20, "height": 10},
                }
            ],
            "isCustomsDeclarable": False,
            "description": f"Order {order_number}",
            "incoterm": "DAP",
            "unitOfMeasurement": "metric",
        },
    }

    try:
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.post(
                url,
                json=payload,
                auth=(settings.api_key or "", settings.api_secret or ""),
                headers={"Accept": "application/json", "Content-Type": "application/json"},
            )
        if resp.status_code not in (200, 201):
            logger.warning("[dhl] create_shipment non-2xx %s %s", resp.status_code, resp.text[:300])
            # Fall back to stub so the admin flow still completes.
            tracking_number = _stub_tracking_number(f"{order_number}:fallback")
            return {
                "tracking_number": tracking_number,
                "label_url": None,
                "stub": True,
                "message": f"DHL API returned HTTP {resp.status_code}; using demo tracking number.",
            }
        data = resp.json()
        tracking_number = (
            data.get("shipmentTrackingNumber")
            or data.get("trackingUrl")
            or _stub_tracking_number(order_number)
        )
        label_url = None
        for doc in data.get("documents", []) or []:
            if doc.get("typeCode") in ("label", "invoice"):
                label_url = doc.get("url") or doc.get("content")
                break
        return {
            "tracking_number": str(tracking_number),
            "label_url": label_url,
            "stub": False,
            "message": "DHL shipment created.",
        }
    except httpx.HTTPError as exc:
        logger.warning("[dhl] create_shipment network error: %s", exc)
        tracking_number = _stub_tracking_number(f"{order_number}:neterr")
        return {
            "tracking_number": tracking_number,
            "label_url": None,
            "stub": True,
            "message": f"DHL network error: {exc}. Returning demo tracking number.",
        }


async def get_tracking(
    settings: DhlSettings,
    tracking_number: str,
    *,
    created_at: Optional[datetime] = None,
) -> Dict[str, Any]:
    """Return tracking events for a shipment.

    If credentials are missing, or the DHL API fails, we return deterministic
    stub events so the customer tracking page always works for demos.
    """

    tracking_number = (tracking_number or "").strip()
    if not tracking_number:
        raise DhlError("tracking_number is required")

    reference_time = created_at or datetime.now(timezone.utc) - timedelta(hours=6)
    if reference_time.tzinfo is None:
        reference_time = reference_time.replace(tzinfo=timezone.utc)

    if not settings.configured:
        return {
            "tracking_number": tracking_number,
            "status": "in_transit",
            "stub": True,
            "events": _stub_events(tracking_number, reference_time),
            "message": "Demo tracking data (DHL credentials not configured).",
        }

    base_url = _DHL_BASE_URLS[settings.environment]
    url = f"{base_url}/tracking"
    params = {"shipmentTrackingNumber": tracking_number}
    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            resp = await client.get(
                url,
                params=params,
                auth=(settings.api_key or "", settings.api_secret or ""),
                headers={"Accept": "application/json"},
            )
        if resp.status_code != 200:
            return {
                "tracking_number": tracking_number,
                "status": "unknown",
                "stub": True,
                "events": _stub_events(tracking_number, reference_time),
                "message": f"DHL API returned HTTP {resp.status_code}; showing demo events.",
            }
        data = resp.json()
        events: List[Dict[str, Any]] = []
        shipments = data.get("shipments") or []
        latest_status = "in_transit"
        for shipment in shipments:
            latest_status = shipment.get("status", latest_status) or latest_status
            for evt in shipment.get("events", []) or []:
                ts = evt.get("date") or evt.get("timestamp")
                events.append(
                    {
                        "timestamp": ts,
                        "description": evt.get("description") or evt.get("typeCode") or "Update",
                        "location": (
                            (evt.get("location") or {}).get("address", {}).get("cityName")
                            or evt.get("serviceArea", [{}])[0].get("description", "")
                        ),
                    }
                )
        if not events:
            events = _stub_events(tracking_number, reference_time)
        return {
            "tracking_number": tracking_number,
            "status": latest_status,
            "stub": False,
            "events": events,
            "message": "Live tracking data from DHL.",
        }
    except httpx.HTTPError as exc:
        logger.warning("[dhl] get_tracking network error: %s", exc)
        return {
            "tracking_number": tracking_number,
            "status": "unknown",
            "stub": True,
            "events": _stub_events(tracking_number, reference_time),
            "message": f"DHL network error: {exc}. Showing demo events.",
        }