import json
import logging
from decimal import Decimal
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from uuid import uuid4

from fastapi import HTTPException

from app.core.config import settings
from app.core.database import execute_query, execute_query_single, table_has_column
from app.modules.fedex.backend.api_client import FedExApiClient, parse_tracking_events
from app.modules.fedex.models.schemas import FedExBookingCreate

logger = logging.getLogger(__name__)


def _json_default(value: Any) -> Any:
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, datetime):
        return value.isoformat()
    return str(value)


def _json_dumps(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, default=_json_default)


def _to_float(value: Any) -> Optional[float]:
    try:
        if value is None or value == "":
            return None
        return float(value)
    except (TypeError, ValueError):
        return None


def _extract_price_info(payload: Dict[str, Any]) -> tuple[Optional[float], Optional[str]]:
    if not isinstance(payload, dict):
        return None, None

    direct_amount = _to_float(
        payload.get("total_amount")
        or payload.get("totalAmount")
        or payload.get("total_cost")
        or payload.get("totalCost")
        or payload.get("price")
        or payload.get("amount")
    )
    direct_currency = (
        payload.get("currency")
        or payload.get("currencyCode")
        or payload.get("total_cost_currency")
    )
    if direct_amount is not None:
        return direct_amount, str(direct_currency or "").upper() or None

    stack: List[Any] = [payload]
    visited: set[int] = set()

    while stack:
        node = stack.pop()
        node_id = id(node)
        if node_id in visited:
            continue
        visited.add(node_id)

        if isinstance(node, dict):
            amount = _to_float(node.get("amount") or node.get("value"))
            currency = node.get("currency") or node.get("currencyCode")
            if amount is not None and currency:
                return amount, str(currency).upper()

            # Prioritize common FedEx charge keys if present.
            for key in (
                "totalNetCharge",
                "totalNetFedExCharge",
                "totalBaseCharge",
                "totalSurcharges",
                "netCharge",
            ):
                nested = node.get(key)
                if isinstance(nested, dict):
                    nested_amount = _to_float(nested.get("amount") or nested.get("value"))
                    nested_currency = nested.get("currency") or nested.get("currencyCode")
                    if nested_amount is not None:
                        return nested_amount, str(nested_currency or "").upper() or None

            stack.extend(node.values())
        elif isinstance(node, list):
            stack.extend(node)

    return None, None


def _extract_label_url(payload: Dict[str, Any]) -> Optional[str]:
    if not isinstance(payload, dict):
        return None

    direct = payload.get("label_url") or payload.get("labelUrl") or payload.get("label")
    if isinstance(direct, str) and direct.strip().lower().startswith(("http://", "https://")):
        return direct.strip()

    stack: List[Any] = [payload]
    visited: set[int] = set()
    while stack:
        node = stack.pop()
        node_id = id(node)
        if node_id in visited:
            continue
        visited.add(node_id)

        if isinstance(node, dict):
            for key, value in node.items():
                key_lower = str(key).lower()
                if isinstance(value, str):
                    v = value.strip()
                    if v.lower().startswith(("http://", "https://")) and (
                        "label" in key_lower or "document" in key_lower or "url" in key_lower
                    ):
                        return v
                elif isinstance(value, (dict, list)):
                    stack.append(value)
        elif isinstance(node, list):
            stack.extend(node)

    return None


def _extract_tracking_number(payload: Dict[str, Any]) -> Optional[str]:
    if not isinstance(payload, dict):
        return None

    direct = (
        payload.get("tracking_number")
        or payload.get("trackingNumber")
        or payload.get("masterTrackingNumber")
    )
    if direct is not None:
        value = str(direct).strip()
        if value:
            return value

    stack: List[Any] = [payload]
    visited: set[int] = set()
    while stack:
        node = stack.pop()
        node_id = id(node)
        if node_id in visited:
            continue
        visited.add(node_id)

        if isinstance(node, dict):
            for key, value in node.items():
                key_lower = str(key).lower()
                if "tracking" in key_lower and value is not None and not isinstance(value, (dict, list)):
                    candidate = str(value).strip()
                    if candidate:
                        return candidate
                if isinstance(value, (dict, list)):
                    stack.append(value)
        elif isinstance(node, list):
            stack.extend(node)

    return None


def _build_tracking_url(tracking_number: Optional[str]) -> Optional[str]:
    if not tracking_number:
        return None
    return f"https://www.fedex.com/fedextrack/?trknbr={tracking_number}"


def _estimate_dry_run_price(payload: Dict[str, Any]) -> tuple[float, str]:
    packages = payload.get("packages") if isinstance(payload, dict) else []
    if not isinstance(packages, list) or not packages:
        return 99.0, "DKK"

    total_weight = 0.0
    for p in packages:
        if isinstance(p, dict):
            total_weight += _to_float(p.get("weight_kg")) or 0.0

    estimated = round(79.0 + (total_weight * 8.5), 2)
    return max(estimated, 79.0), "DKK"


class FedExService:
    def __init__(self) -> None:
        self.client = FedExApiClient()

    @property
    def enabled(self) -> bool:
        return bool(settings.FEDEX_ENABLED)

    @property
    def read_only(self) -> bool:
        return bool(settings.FEDEX_READ_ONLY)

    @property
    def dry_run(self) -> bool:
        return bool(settings.FEDEX_DRY_RUN)

    def _assert_enabled(self) -> None:
        if not self.enabled:
            raise HTTPException(status_code=503, detail="FedEx integration is disabled")

    def _booking_ref(self) -> str:
        stamp = datetime.now(timezone.utc).strftime("%Y%m%d")
        return f"FDX-{stamp}-{uuid4().hex[:8].upper()}"

    def _validate_relations(self, payload: FedExBookingCreate) -> None:
        case_exists = execute_query_single("SELECT id FROM sag_sager WHERE id = %s", (payload.case_id,))
        if not case_exists:
            raise HTTPException(status_code=404, detail="Case not found")

        if payload.customer_id:
            customer_exists = execute_query_single("SELECT id FROM customers WHERE id = %s", (payload.customer_id,))
            if not customer_exists:
                raise HTTPException(status_code=404, detail="Customer not found")

        if payload.contact_id:
            contact_exists = execute_query_single("SELECT id FROM contacts WHERE id = %s", (payload.contact_id,))
            if not contact_exists:
                raise HTTPException(status_code=404, detail="Contact not found")

    def _fetch_packages(self, shipment_id: int) -> List[Dict[str, Any]]:
        rows = execute_query(
            """
            SELECT weight_kg, length_cm, width_cm, height_cm, description
            FROM fedex_shipment_packages
            WHERE shipment_id = %s
            ORDER BY id ASC
            """,
            (shipment_id,),
        ) or []
        return [dict(row) for row in rows]

    def _shipment_row_to_dict(self, row: Dict[str, Any]) -> Dict[str, Any]:
        mapped = dict(row)
        mapped["packages"] = self._fetch_packages(int(row["id"]))

        api_response = mapped.get("api_response")
        if isinstance(api_response, str):
            try:
                api_response = json.loads(api_response)
            except Exception:
                api_response = None

        if isinstance(api_response, dict):
            if not mapped.get("tracking_number"):
                mapped["tracking_number"] = _extract_tracking_number(api_response)
            if not mapped.get("label_url"):
                mapped["label_url"] = _extract_label_url(api_response)

            if mapped.get("total_amount") is None:
                fallback_amount, fallback_currency = _extract_price_info(api_response)
                if fallback_amount is not None:
                    mapped["total_amount"] = fallback_amount
                if not mapped.get("currency") and fallback_currency:
                    mapped["currency"] = fallback_currency

        mapped["tracking_url"] = _build_tracking_url(mapped.get("tracking_number"))

        # Ensure older dry-run rows still expose useful test outputs in UI.
        if mapped.get("dry_run") and mapped.get("shipment_status") in {"submitted", "booked"}:
            if not mapped.get("label_url") and mapped.get("tracking_url"):
                mapped["label_url"] = mapped["tracking_url"]
            if mapped.get("total_amount") is None:
                estimated_amount, estimated_currency = _estimate_dry_run_price({"packages": mapped.get("packages") or []})
                mapped["total_amount"] = estimated_amount
                if not mapped.get("currency"):
                    mapped["currency"] = estimated_currency

        return mapped

    def create_booking_draft(self, payload: FedExBookingCreate, created_by_user_id: Optional[int]) -> Dict[str, Any]:
        self._assert_enabled()
        self._validate_relations(payload)

        if payload.pickup_window_end <= payload.pickup_window_start:
            raise HTTPException(status_code=400, detail="pickup_window_end must be after pickup_window_start")

        booking_ref = self._booking_ref()
        shipment_row = execute_query_single(
            """
            INSERT INTO fedex_shipments (
                booking_ref,
                case_id,
                customer_id,
                contact_id,
                service_type,
                shipment_status,
                pickup_window_start,
                pickup_window_end,
                recipient_name,
                company_name,
                address_line1,
                address_line2,
                postal_code,
                city,
                country_code,
                phone,
                email,
                dry_run,
                created_by_user_id,
                updated_by_user_id,
                api_payload,
                api_response
            ) VALUES (
                %s, %s, %s, %s, %s, 'draft',
                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
                %s, %s, %s, %s::jsonb, %s::jsonb
            )
            RETURNING *
            """,
            (
                booking_ref,
                payload.case_id,
                payload.customer_id,
                payload.contact_id,
                payload.service_type,
                payload.pickup_window_start,
                payload.pickup_window_end,
                payload.address.recipient_name,
                payload.address.company_name,
                payload.address.address_line1,
                payload.address.address_line2,
                payload.address.postal_code,
                payload.address.city,
                payload.address.country_code.upper(),
                payload.address.phone,
                payload.address.email,
                self.dry_run,
                created_by_user_id,
                created_by_user_id,
                _json_dumps(payload.model_dump(mode="json")),
                _json_dumps({"status": "draft_created"}),
            ),
        )
        if not shipment_row:
            raise HTTPException(status_code=500, detail="Failed to create booking draft")

        shipment_id = int(shipment_row["id"])
        for package in payload.packages:
            execute_query(
                """
                INSERT INTO fedex_shipment_packages (
                    shipment_id, weight_kg, length_cm, width_cm, height_cm, description
                ) VALUES (%s, %s, %s, %s, %s, %s)
                """,
                (
                    shipment_id,
                    package.weight_kg,
                    package.length_cm,
                    package.width_cm,
                    package.height_cm,
                    package.description,
                ),
            )

        logger.info("✅ FedEx draft created: %s (case=%s)", booking_ref, payload.case_id)
        return self._shipment_row_to_dict(dict(shipment_row))

    def list_bookings(self, case_id: Optional[int] = None) -> List[Dict[str, Any]]:
        params: List[Any] = []
        where_sql = "WHERE deleted_at IS NULL"
        if case_id is not None:
            where_sql += " AND case_id = %s"
            params.append(case_id)

        rows = execute_query(
            f"""
            SELECT *
            FROM fedex_shipments
            {where_sql}
            ORDER BY created_at DESC
            LIMIT 200
            """,
            tuple(params),
        ) or []
        return [self._shipment_row_to_dict(dict(row)) for row in rows]

    def get_booking(self, booking_ref: str) -> Dict[str, Any]:
        row = execute_query_single(
            """
            SELECT *
            FROM fedex_shipments
            WHERE booking_ref = %s
              AND deleted_at IS NULL
            """,
            (booking_ref,),
        )
        if not row:
            raise HTTPException(status_code=404, detail="Booking not found")
        return self._shipment_row_to_dict(dict(row))

    async def submit_booking(self, booking_ref: str, user_id: Optional[int]) -> Dict[str, Any]:
        self._assert_enabled()
        shipment = self.get_booking(booking_ref)

        if shipment["shipment_status"] not in {"draft", "failed"}:
            raise HTTPException(status_code=409, detail="Only draft/failed bookings can be submitted")

        if self.read_only:
            raise HTTPException(status_code=403, detail="FedEx write actions are disabled (read-only mode)")

        payload = {
            "booking_ref": shipment["booking_ref"],
            "service_type": shipment["service_type"],
            "pickup_window_start": shipment["pickup_window_start"].isoformat() if shipment.get("pickup_window_start") else None,
            "pickup_window_end": shipment["pickup_window_end"].isoformat() if shipment.get("pickup_window_end") else None,
            "recipient": {
                "recipient_name": shipment["recipient_name"],
                "company_name": shipment.get("company_name"),
                "address_line1": shipment["address_line1"],
                "address_line2": shipment.get("address_line2"),
                "postal_code": shipment["postal_code"],
                "city": shipment["city"],
                "country_code": shipment["country_code"],
                "phone": shipment.get("phone"),
                "email": shipment.get("email"),
            },
            "packages": shipment["packages"],
        }

        if self.dry_run:
            tracking_number = f"DRYRUN-{uuid4().hex[:12].upper()}"
            label_url = _build_tracking_url(tracking_number)
            total_amount, currency = _estimate_dry_run_price(payload)
            api_response = {
                "dry_run": True,
                "tracking_number": tracking_number,
                "label_url": label_url,
                "total_amount": total_amount,
                "currency": currency,
            }
            new_status = "submitted"
        else:
            try:
                api_response = await self.client.create_shipment(payload)
            except Exception as exc:
                execute_query(
                    """
                    UPDATE fedex_shipments
                    SET shipment_status = 'failed',
                        api_response = %s::jsonb,
                        updated_by_user_id = %s,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE booking_ref = %s
                    """,
                    (_json_dumps({"error": str(exc)}), user_id, booking_ref),
                )
                raise HTTPException(status_code=502, detail="FedEx booking failed") from exc

            tracking_number = _extract_tracking_number(api_response)
            label_url = _extract_label_url(api_response)
            new_status = "booked"
            total_amount, currency = _extract_price_info(api_response)

        has_total_amount = table_has_column("fedex_shipments", "total_amount")
        has_currency = table_has_column("fedex_shipments", "currency")

        set_clauses = [
            "shipment_status = %s",
            "tracking_number = COALESCE(%s, tracking_number)",
            "label_url = COALESCE(%s, label_url)",
        ]
        update_params: List[Any] = [
            new_status,
            tracking_number,
            label_url,
        ]

        if has_total_amount:
            set_clauses.append("total_amount = COALESCE(%s, total_amount)")
            update_params.append(total_amount)
        if has_currency:
            set_clauses.append("currency = COALESCE(%s, currency)")
            update_params.append(currency)

        set_clauses.extend([
            "submitted_at = CURRENT_TIMESTAMP",
            "api_payload = %s::jsonb",
            "api_response = %s::jsonb",
            "updated_by_user_id = %s",
            "updated_at = CURRENT_TIMESTAMP",
        ])
        update_params.extend([
            _json_dumps(payload),
            _json_dumps(api_response),
            user_id,
            booking_ref,
        ])

        updated = execute_query_single(
            f"""
            UPDATE fedex_shipments
            SET {', '.join(set_clauses)}
            WHERE booking_ref = %s
            RETURNING *
            """,
            tuple(update_params),
        )

        if tracking_number:
            execute_query(
                """
                INSERT INTO fedex_tracking_events (
                    shipment_id, status, description, event_timestamp
                ) VALUES (%s, %s, %s, CURRENT_TIMESTAMP)
                """,
                (
                    updated["id"],
                    "submitted" if self.dry_run else "booked",
                    "Shipment submitted from BMC Hub",
                ),
            )

        return {
            "booking_ref": booking_ref,
            "status": new_status,
            "dry_run": self.dry_run,
            "tracking_number": tracking_number,
            "tracking_url": _build_tracking_url(tracking_number),
            "label_url": label_url,
            "total_amount": total_amount,
            "currency": currency,
        }

    async def get_tracking(self, booking_ref: str) -> Dict[str, Any]:
        self._assert_enabled()
        shipment = self.get_booking(booking_ref)

        tracking_number = shipment.get("tracking_number")
        if not tracking_number:
            events = execute_query(
                """
                SELECT status, event_timestamp, description, location_city, location_country
                FROM fedex_tracking_events
                WHERE shipment_id = %s
                ORDER BY event_timestamp DESC
                """,
                (shipment["id"],),
            ) or []
            return {
                "booking_ref": booking_ref,
                "shipment_status": shipment["shipment_status"],
                "tracking_number": None,
                "events": [dict(row) for row in events],
            }

        if self.dry_run:
            events = execute_query(
                """
                SELECT status, event_timestamp, description, location_city, location_country
                FROM fedex_tracking_events
                WHERE shipment_id = %s
                ORDER BY event_timestamp DESC
                """,
                (shipment["id"],),
            ) or []
            return {
                "booking_ref": booking_ref,
                "shipment_status": shipment["shipment_status"],
                "tracking_number": tracking_number,
                "events": [dict(row) for row in events],
            }

        try:
            provider_payload = await self.client.get_tracking(tracking_number)
        except Exception as exc:
            raise HTTPException(status_code=502, detail="Failed to fetch FedEx tracking") from exc

        events = parse_tracking_events(provider_payload)
        if events:
            execute_query(
                "DELETE FROM fedex_tracking_events WHERE shipment_id = %s",
                (shipment["id"],),
            )
            for event in events:
                execute_query(
                    """
                    INSERT INTO fedex_tracking_events (
                        shipment_id,
                        status,
                        description,
                        event_timestamp,
                        location_city,
                        location_country
                    ) VALUES (
                        %s, %s, %s,
                        COALESCE(%s::timestamp, CURRENT_TIMESTAMP),
                        %s, %s
                    )
                    """,
                    (
                        shipment["id"],
                        event.get("status") or "unknown",
                        event.get("description"),
                        event.get("event_timestamp"),
                        event.get("location_city"),
                        event.get("location_country"),
                    ),
                )

        status = str(provider_payload.get("shipment_status") or shipment["shipment_status"])
        execute_query(
            """
            UPDATE fedex_shipments
            SET shipment_status = %s,
                api_response = %s::jsonb,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = %s
            """,
            (status, _json_dumps(provider_payload), shipment["id"]),
        )

        current_events = execute_query(
            """
            SELECT status, event_timestamp, description, location_city, location_country
            FROM fedex_tracking_events
            WHERE shipment_id = %s
            ORDER BY event_timestamp DESC
            """,
            (shipment["id"],),
        ) or []

        return {
            "booking_ref": booking_ref,
            "shipment_status": status,
            "tracking_number": tracking_number,
            "events": [dict(row) for row in current_events],
        }

    async def cancel_booking(self, booking_ref: str, reason: Optional[str], user_id: Optional[int]) -> Dict[str, Any]:
        self._assert_enabled()
        if self.read_only:
            raise HTTPException(status_code=403, detail="FedEx write actions are disabled (read-only mode)")

        shipment = self.get_booking(booking_ref)
        if shipment["shipment_status"] == "cancelled":
            return {"booking_ref": booking_ref, "status": "cancelled", "cancelled": True}

        if not self.dry_run and shipment.get("tracking_number"):
            try:
                await self.client.cancel_shipment(str(shipment["tracking_number"]))
            except Exception as exc:
                raise HTTPException(status_code=502, detail="Failed to cancel shipment at FedEx") from exc

        execute_query(
            """
            UPDATE fedex_shipments
            SET shipment_status = 'cancelled',
                cancel_reason = %s,
                updated_by_user_id = %s,
                updated_at = CURRENT_TIMESTAMP
            WHERE booking_ref = %s
            """,
            (reason, user_id, booking_ref),
        )

        execute_query(
            """
            INSERT INTO fedex_tracking_events (shipment_id, status, description, event_timestamp)
            VALUES (%s, 'cancelled', %s, CURRENT_TIMESTAMP)
            """,
            (shipment["id"], reason or "Cancelled from BMC Hub"),
        )

        return {"booking_ref": booking_ref, "status": "cancelled", "cancelled": True}


fedex_service = FedExService()
