import { ConvexError } from "convex/values";
import type {
  AddressValidationResult,
  RecommendedAddress,
  ShippoRate,
  ValidatedCartItem,
} from "./checkout";

type ValidateAddressInput = {
  addressLine1: string;
  additionalInformation?: string;
  city: string;
  postalCode: string;
  country: string;
  name?: string;
};

type ShippoRawResponse = {
  original_address: {
    address_line_1: string;
    address_line_2?: string;
    city_locality: string;
    state_province: string;
    postal_code: string;
    country_code: string;
    name?: string;
    organization?: string;
  };
  recommended_address?: {
    address_line_1: string;
    address_line_2?: string;
    city_locality: string;
    state_province: string;
    postal_code: string;
    country_code: string;
    complete_address?: string;
    confidence_result: {
      score: "high" | "medium" | "low";
      code: string;
      description: string;
    };
  };
  analysis: {
    validation_result: {
      value: "valid" | "partially_valid" | "invalid";
      reasons: Array<{ code: string; description: string }>;
    };
    address_type:
      | "residential"
      | "commercial"
      | "unknown"
      | "po_box"
      | "military";
    changed_attributes?: string[];
  };
  geo?: {
    latitude: number;
    longitude: number;
  };
};

const SHIPPO_VALIDATE_URL =
  "https://api.goshippo.com/v2/addresses/validate";

/**
 * Calls Shippo Address Validation v2 and normalizes the response into
 * an `AddressValidationResult`. This is a pure async helper — it does NOT
 * export a Convex function; it's consumed by actions in `checkoutActions.ts`.
 */
export async function validateAddressWithShippo(
  input: ValidateAddressInput,
): Promise<AddressValidationResult> {
  const apiKey = process.env.SHIPPO_API_KEY;
  if (!apiKey) {
    throw new ConvexError(
      "Address validation is unavailable (missing API key configuration).",
    );
  }

  const params = new URLSearchParams();
  params.set("address_line_1", input.addressLine1);
  if (input.additionalInformation)
    params.set("address_line_2", input.additionalInformation);
  params.set("city_locality", input.city);
  params.set("postal_code", input.postalCode);
  params.set("country_code", input.country);
  if (input.name) params.set("name", input.name);

  let response: Response;
  try {
    response = await fetch(`${SHIPPO_VALIDATE_URL}?${params.toString()}`, {
      method: "GET",
      headers: { Authorization: `ShippoToken ${apiKey}` },
    });
  } catch (err) {
    throw new ConvexError(
      "Address validation service is unreachable. Please try again later.",
    );
  }

  if (!response.ok) {
    throw new ConvexError(
      `Address validation service unavailable (status ${response.status}).`,
    );
  }

  let body: ShippoRawResponse;
  try {
    body = (await response.json()) as ShippoRawResponse;
  } catch {
    throw new ConvexError(
      "Address validation returned an unexpected response. Please try again.",
    );
  }

  if (!body.analysis?.validation_result) {
    throw new ConvexError(
      "Address validation returned a malformed response.",
    );
  }

  const { analysis, recommended_address, original_address } = body;

  let recommendedAddress: RecommendedAddress | undefined;
  if (recommended_address) {
    recommendedAddress = {
      addressLine1: recommended_address.address_line_1,
      additionalInformation: recommended_address.address_line_2,
      city: recommended_address.city_locality,
      postalCode: recommended_address.postal_code,
      country: recommended_address.country_code,
      completeAddress: recommended_address.complete_address,
      confidenceScore: recommended_address.confidence_result.score,
      confidenceCode: recommended_address.confidence_result.code,
      confidenceDescription:
        recommended_address.confidence_result.description,
    };
  }

  return {
    isValid: analysis.validation_result.value === "valid",
    validationValue: analysis.validation_result.value,
    reasons: analysis.validation_result.reasons.map((r) => ({
      code: r.code,
      description: r.description,
    })),
    addressType: analysis.address_type,
    changedAttributes: analysis.changed_attributes ?? [],
    recommendedAddress,
    originalAddress: {
      addressLine1: original_address.address_line_1,
      additionalInformation: original_address.address_line_2,
      city: original_address.city_locality,
      postalCode: original_address.postal_code,
      country: original_address.country_code,
    },
  };
}

// ─── Shipping Rates Helpers ──────────────────────────────────────────────────

export const PREFERRED_CARRIERS = ["DPD UK", "Evri UK", "UPS", "UDS"];

/**
 * Hard ceiling across preferred UK carriers.
 * DPD UK premium (door-to-door, Saturday/Sunday): 30kg
 * DPD UK standard (Classic, Next Day, Two Day):   20kg
 * Evri UK (Courier Collection, ParcelShop):        15kg
 */
export const MAX_PARCEL_WEIGHT_G = 30_000;

const WEIGHT_TO_GRAMS: Record<ValidatedCartItem["weightUnit"], number> = {
  g: 1,
  kg: 1000,
  lb: 453.592,
  oz: 28.3495,
};

const DIMENSION_TO_CM: Record<NonNullable<ValidatedCartItem["dimensionUnit"]>, number> = {
  cm: 1,
  in: 2.54,
};

type ParcelResult = {
  weight: string;
  mass_unit: "g";
  length?: string;
  width?: string;
  height?: string;
  distance_unit?: "cm";
};

export function computeParcel(items: ValidatedCartItem[]): ParcelResult {
  let totalWeightGrams = 0;
  for (const item of items) {
    const factor = WEIGHT_TO_GRAMS[item.weightUnit];
    totalWeightGrams += item.weight * factor * item.quantity;
  }

  const withDimensions = items.filter(
    (item): item is ValidatedCartItem & { length: number; width: number; height: number; dimensionUnit: "cm" | "in" } =>
      item.length != null && item.width != null && item.height != null && item.dimensionUnit != null,
  );

  if (withDimensions.length === 0) {
    return { weight: String(Math.round(totalWeightGrams)), mass_unit: "g" };
  }

  let maxLengthCm = 0;
  let maxWidthCm = 0;
  let totalHeightCm = 0;

  for (const item of withDimensions) {
    const factor = DIMENSION_TO_CM[item.dimensionUnit];
    const lengthCm = item.length * factor;
    const widthCm = item.width * factor;
    const heightCm = item.height * factor * item.quantity;

    if (lengthCm > maxLengthCm) maxLengthCm = lengthCm;
    if (widthCm > maxWidthCm) maxWidthCm = widthCm;
    totalHeightCm += heightCm;
  }

  return {
    weight: String(Math.round(totalWeightGrams)),
    mass_unit: "g",
    length: String(Math.round(maxLengthCm * 100) / 100),
    width: String(Math.round(maxWidthCm * 100) / 100),
    height: String(Math.round(totalHeightCm * 100) / 100),
    distance_unit: "cm",
  };
}

const SHIPPO_SHIPMENTS_URL = "https://api.goshippo.com/shipments/";

export async function getShippingRatesFromShippo(input: {
  sourceAddressId: string;
  destinationAddress: {
    name: string;
    street1: string;
    street2?: string;
    city: string;
    zip: string;
    country: string;
    phone?: string;
  };
  parcels: Array<{
    weight: string;
    mass_unit: string;
    length?: string;
    width?: string;
    height?: string;
    distance_unit?: string;
  }>;
}): Promise<{ shipmentObjectId: string; rates: ShippoRate[] }> {
  const apiKey = process.env.SHIPPO_API_KEY;
  if (!apiKey) {
    throw new ConvexError(
      "Shipping rate service is unavailable (missing API key configuration).",
    );
  }

  const requestBody = {
    address_from: input.sourceAddressId,
    address_to: input.destinationAddress,
    parcels: input.parcels,
    async: false,
  };

  let response: Response;
  try {
    response = await fetch(SHIPPO_SHIPMENTS_URL, {
      method: "POST",
      headers: {
        Authorization: `ShippoToken ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(requestBody),
    });
  } catch {
    throw new ConvexError(
      "Shipping rate service is unreachable. Please try again later.",
    );
  }

  if (!response.ok) {
    let errorDetail = "";
    try {
      const errBody = await response.json();
      errorDetail = JSON.stringify(errBody);
      console.error("Shippo /shipments/ error:", response.status, errorDetail);
    } catch {
      console.error("Shippo /shipments/ error:", response.status, "(no parseable body)");
    }
    throw new ConvexError(
      `Shipping rate service unavailable (status ${response.status}).`,
    );
  }

  let body: {
    object_id: string;
    messages?: Array<{ source: string; text: string }>;
    rates: Array<{
      object_id: string;
      provider: string;
      servicelevel: { name: string; token: string };
      amount: string;
      currency: string;
      estimated_days: number;
      duration_terms: string;
      arrives_by?: string | null;
      carrier_account: string;
    }>;
  };
  try {
    body = await response.json();
  } catch {
    throw new ConvexError(
      "Shipping rate service returned an unexpected response. Please try again.",
    );
  }

  if (body.rates.length === 0 && body.messages?.length) {
    console.warn(
      "Shippo returned 0 rates. Carrier messages:",
      body.messages.map((m) => `[${m.source}] ${m.text}`).join(" | "),
    );
  }

  const rates: ShippoRate[] = body.rates.map((rate) => ({
    objectId: rate.object_id,
    provider: rate.provider,
    servicelevelName: rate.servicelevel.name,
    servicelevelToken: rate.servicelevel.token,
    amount: rate.amount,
    currency: rate.currency,
    estimatedDays: rate.estimated_days,
    durationTerms: rate.duration_terms,
    arrivesBy: rate.arrives_by ?? null,
    carrierAccount: rate.carrier_account,
  }));

  return { shipmentObjectId: body.object_id, rates };
}

/**
 * Fetches a Shippo shipment by ID and returns the `object_id` of the rate
 * matching `serviceCode` (servicelevel.token) and `carrier` (provider).
 * Throws a ConvexError if the shipment is not found, the rate is missing,
 * or the API is unreachable.
 */
export async function getShipmentRateObjectId(
  shipmentId: string,
  serviceCode: string,
  carrier: string,
): Promise<string> {
  const apiKey = process.env.SHIPPO_API_KEY;
  if (!apiKey) {
    throw new ConvexError("Shipping service unavailable (missing API key).");
  }

  let response: Response;
  try {
    response = await fetch(`${SHIPPO_SHIPMENTS_URL}${shipmentId}`, {
      method: "GET",
      headers: { Authorization: `ShippoToken ${apiKey}` },
    });
  } catch {
    throw new ConvexError("Shippo service is unreachable. Please try again.");
  }

  if (response.status === 404) {
    throw new ConvexError(`Shipment "${shipmentId}" not found in Shippo.`);
  }
  if (!response.ok) {
    throw new ConvexError(`Shippo service error (status ${response.status}).`);
  }

  let body: {
    object_id: string;
    rates: Array<{
      object_id: string;
      provider: string;
      servicelevel: { token: string; name: string };
    }>;
  };
  try {
    body = await response.json();
  } catch {
    throw new ConvexError("Shippo returned an unexpected response.");
  }

  const matching = body.rates.filter(
    (r) =>
      r.servicelevel.token === serviceCode &&
      r.provider.toLowerCase() === carrier.toLowerCase(),
  );

  if (matching.length === 0) {
    throw new ConvexError(
      `No rate found for service "${serviceCode}" and carrier "${carrier}". The rate may have expired.`,
    );
  }

  return matching[0].object_id;
}

export function selectBestRate(rates: ShippoRate[]): {
  selected: ShippoRate;
  alternatives: ShippoRate[];
} {
  if (rates.length === 0) {
    throw new ConvexError(
      "No shipping rates available for this address. Please verify your address and try again.",
    );
  }

  const preferredLower = PREFERRED_CARRIERS.map((c) => c.toLowerCase());

  const preferred = rates.filter((r) =>
    preferredLower.includes(r.provider.toLowerCase()),
  );

  const sortByDaysThenPrice = (a: ShippoRate, b: ShippoRate) => {
    const aDays = a.estimatedDays ?? Infinity;
    const bDays = b.estimatedDays ?? Infinity;
    const daysDiff = aDays - bDays;
    if (daysDiff !== 0) return daysDiff;
    return parseFloat(a.amount) - parseFloat(b.amount);
  };

  if (preferred.length > 0) {
    preferred.sort(sortByDaysThenPrice);
    return { selected: preferred[0], alternatives: preferred.slice(1, 3) };
  }

  console.warn(
    "No preferred carriers returned rates. Falling back to all carriers.",
  );
  const sorted = [...rates].sort(sortByDaysThenPrice);
  return { selected: sorted[0], alternatives: sorted.slice(1, 3) };
}
