"""Serviço Packlink para ClientFlow.

Implementa a primeira integração segura:
- normalização de códigos postais PT para cotação;
- criação de payload de envio a partir da oportunidade/preparação;
- criação do envio via Packlink e registo em operation_links.
"""

from __future__ import annotations

import json
import os
import re
import uuid
from datetime import date, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple

from sqlalchemy import text

from app.config import settings
from app.db import engine
from app.integration_outbox_service import create_outbox_item
from app.commercial_service import get_customer_for_opportunity
from app.operation_service import register_operation_action
from app.opportunity_service import get_opportunity
from app.packlink_client import PacklinkClient, PacklinkError
from app.product_service import list_opportunity_items
from app.workflow_guard import validate_operation_action


class PacklinkPayloadError(ValueError):
    pass


def _env(name: str, default: str = "") -> str:
    return os.getenv(name, default).strip()


def _env_float(name: str, default: float) -> float:
    value = _env(name)
    if not value:
        return default
    try:
        return float(value)
    except ValueError:
        return default


def _as_float(value: Any, default: float = 0.0) -> float:
    if value is None:
        return default
    if isinstance(value, Decimal):
        return float(value)
    try:
        return float(value)
    except Exception:
        return default


def _compact(value: Any, limit: int = 120) -> str:
    value = re.sub(r"\s+", " ", str(value or "")).strip()
    return value[:limit].strip()


def normalize_packlink_zip(country: str, zip_code: str, *, for_quote: bool = True) -> str:
    """Normaliza código postal para Packlink.

    Nos testes reais de PT, /services aceitou 4000 mas rejeitou 4000-001.
    Para moradas de envio pode ser útil manter o código completo; para cotação
    usamos os 4 primeiros dígitos.
    """
    country = str(country or "").upper().strip()
    zip_code = str(zip_code or "").strip()
    if country == "PT" and for_quote:
        match = re.search(r"\d{4}", zip_code)
        if match:
            return match.group(0)
    return zip_code


def default_package() -> Dict[str, Any]:
    return {
        "height": int(_env_float("PACKLINK_DEFAULT_PACKAGE_HEIGHT", 10)),
        "width": int(_env_float("PACKLINK_DEFAULT_PACKAGE_WIDTH", 20)),
        "length": int(_env_float("PACKLINK_DEFAULT_PACKAGE_LENGTH", 30)),
        "weight": round(_env_float("PACKLINK_DEFAULT_PACKAGE_WEIGHT", 2), 2),
    }


def get_latest_shipment_preparation_for_opportunity(opportunity_id: str) -> Optional[Dict[str, Any]]:
    sql = text("""
        SELECT tp.*
        FROM task_preparations tp
        JOIN tasks t ON t.id = tp.task_id
        WHERE t.opportunity_id = CAST(:opportunity_id AS UUID)
          AND tp.prep_type IN ('shipment', 'pickup')
        ORDER BY tp.created_at DESC
        LIMIT 1
    """)
    with engine.begin() as conn:
        row = conn.execute(sql, {"opportunity_id": opportunity_id}).mappings().first()
    return dict(row) if row else None


def split_pt_address(address: str) -> Tuple[str, str, str]:
    """Extrai rua, código postal e cidade de uma morada PT em texto livre."""
    address = str(address or "").strip()
    if not address:
        return "", "", ""
    clean = re.sub(r"\s+", " ", address.replace("\n", " ")).strip()
    match = re.search(r"\b(\d{4}-\d{3}|\d{4})\b\s*(.*)$", clean)
    if not match:
        return clean, "", ""
    zip_code = match.group(1)
    city = _compact(match.group(2), 80)
    street = clean[: match.start()].strip(" ,-")
    return street, zip_code, city


def _metadata_dict(opp: Dict[str, Any]) -> Dict[str, Any]:
    value = opp.get("metadata") or {}
    if isinstance(value, dict):
        return value
    try:
        return json.loads(value)
    except Exception:
        return {}


def _shipment_data_from_sources(opportunity: Dict[str, Any]) -> Dict[str, Any]:
    metadata = _metadata_dict(opportunity)
    shipment = {}
    for key in ("packlink", "shipment", "shipping"):
        if isinstance(metadata.get(key), dict):
            shipment.update(metadata[key])

    prep = get_latest_shipment_preparation_for_opportunity(str(opportunity["id"]))
    if prep:
        extracted = prep.get("extracted_data") or {}
        if isinstance(extracted, str):
            try:
                extracted = json.loads(extracted)
            except Exception:
                extracted = {}
        if isinstance(extracted, dict):
            prep_shipment = extracted.get("shipment") or {}
            if isinstance(prep_shipment, dict):
                # A preparação é mais recente, mas não deve apagar campos já explícitos.
                shipment = {**prep_shipment, **shipment}

    return shipment


def build_to_address(opportunity: Dict[str, Any]) -> Dict[str, Any]:
    shipment = _shipment_data_from_sources(opportunity)
    linked_customer = None
    try:
        linked_customer = get_customer_for_opportunity(str(opportunity.get("id")))
    except Exception:
        linked_customer = None
    linked_customer = linked_customer or {}

    delivery_address = (
        shipment.get("delivery_address")
        or shipment.get("address")
        or shipment.get("street1")
        or linked_customer.get("street_name")
        or ""
    )
    street, parsed_zip, parsed_city = split_pt_address(delivery_address)

    country = _compact(shipment.get("country") or shipment.get("recipient_country") or linked_customer.get("country") or "PT", 2).upper() or "PT"
    zip_code = _compact(shipment.get("zip_code") or shipment.get("postal_code") or linked_customer.get("postal_zone") or parsed_zip, 20)
    city = _compact(shipment.get("city") or linked_customer.get("city_name") or parsed_city, 80)
    street1 = _compact(shipment.get("street1") or street or linked_customer.get("street_name") or delivery_address, 120)

    name = _compact(
        shipment.get("recipient_name")
        or shipment.get("name")
        or linked_customer.get("name")
        or opportunity.get("customer_name")
        or opportunity.get("customer_email")
        or "Cliente",
        60,
    )
    phone = _compact(shipment.get("recipient_phone") or linked_customer.get("phone") or opportunity.get("customer_phone") or _env("PACKLINK_FALLBACK_PHONE"), 30)
    email = _compact(shipment.get("recipient_email") or linked_customer.get("email") or opportunity.get("customer_email") or _env("PACKLINK_FALLBACK_EMAIL"), 120)

    missing = []
    if not street1:
        missing.append("morada de entrega")
    if not zip_code:
        missing.append("código postal de entrega")
    if not city:
        missing.append("cidade de entrega")
    if not phone:
        missing.append("telefone do destinatário")
    if not email:
        missing.append("email do destinatário")
    if missing:
        raise PacklinkPayloadError("Dados em falta para Packlink: " + ", ".join(missing))

    return {
        "name": name,
        "surname": _compact(shipment.get("recipient_surname") or ".", 60),
        "company": _compact(shipment.get("company") or "", 80),
        "street1": street1,
        "street2": _compact(shipment.get("street2") or "", 120),
        "zip_code": zip_code,
        "city": city,
        "country": country,
        "phone": phone,
        "email": email,
    }


def build_from_address() -> Dict[str, Any]:
    required = {
        "PACKLINK_SENDER_NAME": _env("PACKLINK_SENDER_NAME"),
        "PACKLINK_SENDER_STREET1": _env("PACKLINK_SENDER_STREET1"),
        "PACKLINK_SENDER_ZIP": _env("PACKLINK_SENDER_ZIP"),
        "PACKLINK_SENDER_CITY": _env("PACKLINK_SENDER_CITY"),
        "PACKLINK_SENDER_PHONE": _env("PACKLINK_SENDER_PHONE"),
        "PACKLINK_SENDER_EMAIL": _env("PACKLINK_SENDER_EMAIL"),
    }
    missing = [k for k, v in required.items() if not v]
    if missing:
        raise PacklinkPayloadError("Configuração Packlink remetente em falta: " + ", ".join(missing))

    return {
        "name": required["PACKLINK_SENDER_NAME"],
        "surname": _env("PACKLINK_SENDER_SURNAME", "."),
        "company": _env("PACKLINK_SENDER_COMPANY"),
        "street1": required["PACKLINK_SENDER_STREET1"],
        "street2": _env("PACKLINK_SENDER_STREET2"),
        "zip_code": required["PACKLINK_SENDER_ZIP"],
        "city": required["PACKLINK_SENDER_CITY"],
        "country": _env("PACKLINK_SENDER_COUNTRY", "PT"),
        "phone": required["PACKLINK_SENDER_PHONE"],
        "email": required["PACKLINK_SENDER_EMAIL"],
    }


def default_collection_date() -> str:
    # Dia seguinte; a Packlink devolve datas reais em /services, mas isto é um fallback.
    return (date.today() + timedelta(days=int(_env_float("PACKLINK_COLLECTION_DAYS_AHEAD", 1)))).strftime("%Y/%m/%d")


def build_packlink_shipment_payload(opportunity_id: str) -> Dict[str, Any]:
    opportunity = get_opportunity(opportunity_id)
    if not opportunity:
        raise PacklinkPayloadError(f"Oportunidade não encontrada: {opportunity_id}")

    to_address = build_to_address(opportunity)
    from_address = build_from_address()
    items = list_opportunity_items(opportunity_id)
    content_names = [str(i.get("product_name") or i.get("sku") or "Produto") for i in items] or [opportunity.get("product_interest") or "Produto"]
    content_value = sum(_as_float(i.get("total_price")) for i in items) or _as_float(opportunity.get("value_amount"), 1.0)

    service_id = _env("PACKLINK_DEFAULT_SERVICE_ID", "20571")
    service_name = _env("PACKLINK_DEFAULT_SERVICE", "Paq 24")
    carrier = _env("PACKLINK_DEFAULT_CARRIER", "Correos Express")

    return {
        "platform": _env("PACKLINK_PLATFORM", "PRO"),
        "platform_country": _env("PACKLINK_PLATFORM_COUNTRY", "UN"),
        "source": _env("PACKLINK_SOURCE", "PRO"),
        "service": service_name,
        "carrier": carrier,
        "service_id": service_id,
        "collection_date": _env("PACKLINK_COLLECTION_DATE") or default_collection_date(),
        "collection_time": _env("PACKLINK_COLLECTION_TIME", "09:00-14:00"),
        "from": from_address,
        "to": to_address,
        "packages": [default_package()],
        "content": content_names[:5],
        "contentvalue": round(float(content_value or 1.0), 2),
        "content_second_hand": False,
        "shipment_custom_reference": _compact(f"CF-{opportunity_id}", 50),
        "priority": False,
        "contentValue_currency": str(opportunity.get("currency") or "EUR"),
        "has_customs": False,
    }


async def quote_opportunity_shipping(opportunity_id: str) -> List[Dict[str, Any]]:
    opportunity = get_opportunity(opportunity_id)
    if not opportunity:
        raise PacklinkPayloadError(f"Oportunidade não encontrada: {opportunity_id}")
    to_address = build_to_address(opportunity)
    from_address = build_from_address()
    client = PacklinkClient()
    return await client.quote_services(
        from_country=from_address["country"],
        from_zip=normalize_packlink_zip(from_address["country"], from_address["zip_code"], for_quote=True),
        to_country=to_address["country"],
        to_zip=normalize_packlink_zip(to_address["country"], to_address["zip_code"], for_quote=True),
        packages=[default_package()],
        source=_env("PACKLINK_SOURCE", "PRO"),
    )


def enqueue_packlink_shipment(opportunity_id: str, *, created_by: str = "operator") -> str:
    validate_operation_action(opportunity_id, "packlink_shipment")
    payload = build_packlink_shipment_payload(opportunity_id)
    outbox_id = create_outbox_item(
        business_event_id=str(uuid.uuid4()),
        target_system="packlink",
        action_type="create_shipment",
        payload={"opportunity_id": opportunity_id, "shipment": payload, "created_by": created_by},
        idempotency_key=f"packlink:create_shipment:{opportunity_id}",
    )
    if not outbox_id:
        raise PacklinkPayloadError("Já existe pedido Packlink pendente/enviado para esta oportunidade")
    return outbox_id


async def create_shipment_from_outbox_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    opportunity_id = str(payload.get("opportunity_id") or "").strip()
    if not opportunity_id:
        raise PacklinkPayloadError("opportunity_id em falta no payload Packlink")
    shipment_payload = payload.get("shipment") or build_packlink_shipment_payload(opportunity_id)

    client = PacklinkClient()
    result = await client.create_shipment(shipment_payload)
    reference = result.get("reference") or result.get("packlink_reference") or result.get("id")
    if not reference:
        raise PacklinkError(f"Packlink não devolveu referência: {result}")

    shipment = None
    labels: List[Any] = []
    tracking: List[Dict[str, Any]] = []
    try:
        shipment = await client.get_shipment(str(reference))
    except Exception:
        shipment = None
    try:
        labels = await client.get_labels(str(reference))
    except Exception:
        labels = []
    try:
        tracking = await client.get_tracking(str(reference))
    except Exception:
        tracking = []

    payload_to_store = {
        "packlink_result": result,
        "shipment": shipment,
        "labels": labels,
        "tracking": tracking,
        "request": shipment_payload,
    }
    external_url = (settings.packlink_public_url or "https://pro.packlink.pt").rstrip("/")
    try:
        from app.commercial_service import upsert_shipment_record, get_customer_for_opportunity
        linked_customer = get_customer_for_opportunity(opportunity_id) or {}
        upsert_shipment_record({
            "customer_id": linked_customer.get("id"),
            "opportunity_id": opportunity_id,
            "external_reference": str(reference),
            "carrier": shipment_payload.get("carrier"),
            "service_id": shipment_payload.get("service_id"),
            "service_name": shipment_payload.get("service"),
            "status": "created",
            "price": result.get("price") or result.get("total_price"),
            "currency": result.get("currency") or shipment_payload.get("contentValue_currency") or "EUR",
            "payload": payload_to_store,
        })
    except Exception:
        pass

    register_operation_action(
        opportunity_id,
        "packlink_shipment",
        external_id=str(reference),
        external_name=f"Packlink {reference}",
        external_url=external_url,
        note="Envio Packlink criado automaticamente pelo outbox.",
        payload=payload_to_store,
        created_by=str(payload.get("created_by") or "system"),
    )
    return {"reference": reference, "shipment": shipment, "labels": labels, "tracking": tracking}
