import type { NextApiRequest, NextApiResponse } from "next";

type Suggestion = {
  provider: "packeta" | "ppl" | "balikovna" | "gls" | "dpd";
  id: string;
  name: string;
  address: string;
  street?: string;
  city?: string;
  zip?: string;
  isNonstop?: boolean;
  distanceKm?: number;
  latitude?: number;
  longitude?: number;
};

type PacketaFeedEntry = {
  kind: string;
  point: Record<string, unknown>;
};

let packetaFeedCache: { expiresAt: number; feeds: PacketaFeedEntry[][] } | null =
  null;
let packetaFeedRequest: Promise<PacketaFeedEntry[][]> | null = null;

function text(value: unknown) {
  return typeof value === "string" || typeof value === "number"
    ? String(value).trim()
    : "";
}

function normalize(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/\s+/g, " ")
    .trim();
}

function decodeXml(value: string) {
  return value
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&");
}

function xmlAttributes(source: string) {
  const result: Record<string, string> = {};
  for (const match of source.matchAll(/([A-Za-z][A-Za-z0-9]*)="([^"]*)"/g)) {
    result[match[1]] = decodeXml(match[2]);
  }
  return result;
}

function requestOrigin(req: NextApiRequest) {
  const forwardedHost = String(req.headers["x-forwarded-host"] || "")
    .split(",")[0]
    .trim();
  const host = forwardedHost || req.headers.host || "localhost:3000";
  const forwardedProtocol = String(req.headers["x-forwarded-proto"] || "")
    .split(",")[0]
    .trim();
  const protocol = forwardedProtocol || "http";
  return `${protocol}://${host}`;
}

async function geocodeAddress(address: string): Promise<{ lat: number; lon: number } | null> {
  const geocodingApiUrl =
    process.env.PPL_GEOCODING_API_URL?.trim() ||
    "https://api.dhl.com/ecs/ppl/geoapify/v1";
  const geocodingGatewayKey =
    process.env.PPL_GEOCODING_GATEWAY_KEY?.trim() ||
    "Y86PtpAkWDAoeHu21traBWdTYgxcMmIr";

  try {
    const geocodingUrl = new URL(
      `${geocodingApiUrl.replace(/\/$/, "")}/geocode/autocomplete`,
    );
    geocodingUrl.searchParams.set("text", address);
    geocodingUrl.searchParams.set("lang", "cs");
    geocodingUrl.searchParams.set("filter", "countrycode:cz");
    geocodingUrl.searchParams.set("format", "json");
    geocodingUrl.searchParams.set("limit", "1");

    const geocodingResponse = await fetch(geocodingUrl, {
      headers: { "dhl-api-key": geocodingGatewayKey },
      signal: AbortSignal.timeout(7000),
    });
    if (geocodingResponse.ok) {
      const geocodingResult = (await geocodingResponse.json()) as {
        results?: Array<{ lat?: unknown; lon?: unknown }>;
      };
      const lat = Number(geocodingResult.results?.[0]?.lat);
      const lon = Number(geocodingResult.results?.[0]?.lon);
      if (Number.isFinite(lat) && Number.isFinite(lon)) return { lat, lon };
    }
  } catch {
    // fallback below
  }

  try {
    const nominatimUrl = new URL("https://nominatim.openstreetmap.org/search");
    nominatimUrl.searchParams.set("q", `${address}, Czechia`);
    nominatimUrl.searchParams.set("format", "json");
    nominatimUrl.searchParams.set("limit", "1");
    nominatimUrl.searchParams.set("countrycodes", "cz");
    const nominatimResponse = await fetch(nominatimUrl, {
      headers: { "User-Agent": "vystrikejto-cart/1.0" },
      signal: AbortSignal.timeout(7000),
    });
    if (!nominatimResponse.ok) return null;
    const nominatimResult = (await nominatimResponse.json()) as Array<{
      lat?: string;
      lon?: string;
    }>;
    const lat = Number(nominatimResult[0]?.lat);
    const lon = Number(nominatimResult[0]?.lon);
    if (Number.isFinite(lat) && Number.isFinite(lon)) return { lat, lon };
  } catch {
    return null;
  }

  return null;
}

async function pplSuggestions(
  address: string,
  origin: string,
): Promise<Suggestion[]> {
  const widgetKey =
    process.env.PPL_WIDGET_API_KEY?.trim() ||
    "ak_87faa40f358e4d80830fea818c0238bf";

  const widgetApiUrl =
    process.env.PPL_WIDGET_API_URL?.trim() ||
    "https://api.dhl.com/ecs/ppl/widget";
  const widgetGatewayKey =
    process.env.PPL_WIDGET_GATEWAY_KEY?.trim() ||
    "lsZnaESt8PbXLa4vyFl1aTwFZyzaLRegatyg6E5TY97YzlV9";
  const requestOrigin =
    process.env.NEXT_PUBLIC_BASE_URL?.trim() || origin || "https://vystrikejto.cz";

  const coords = await geocodeAddress(address);
  if (!coords) return [];
  const { lat: latitude, lon: longitude } = coords;

  const pointsUrl = new URL(
    `${widgetApiUrl.replace(/\/$/, "")}/v1/accessPoints`,
  );
  pointsUrl.searchParams.set("countryCode", "CZ");
  pointsUrl.searchParams.set("latitude", String(latitude));
  pointsUrl.searchParams.set("longitude", String(longitude));
  pointsUrl.searchParams.set("limit", "12");
  pointsUrl.searchParams.set("offset", "0");

  const pointsResponse = await fetch(pointsUrl, {
    headers: {
      "x-widget-key": widgetKey,
      "dhl-api-key": widgetGatewayKey,
      Origin: requestOrigin,
    },
    signal: AbortSignal.timeout(9000),
  });
  if (!pointsResponse.ok) return [];

  const rawPoints = (await pointsResponse.json()) as unknown;
  const points = Array.isArray(rawPoints)
    ? rawPoints
    : rawPoints &&
        typeof rawPoints === "object" &&
        Array.isArray((rawPoints as { data?: unknown[] }).data)
      ? (rawPoints as { data: unknown[] }).data
      : [];
  return points
    .map((raw) =>
      raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {},
    )
    .filter((point) => {
      const activeFeatures =
        point.activeFeatures && typeof point.activeFeatures === "object"
          ? (point.activeFeatures as Record<string, unknown>)
          : {};
      return activeFeatures.vacationMode !== true;
    })
    .slice(0, 8)
    .map((point) => {
      const pointAddress =
        point.address && typeof point.address === "object"
          ? (point.address as Record<string, unknown>)
          : {};
      const openHours =
        point.openHours && typeof point.openHours === "object"
          ? (point.openHours as Record<string, unknown>)
          : {};
      const regularHours = Array.isArray(openHours.regular)
        ? openHours.regular
        : [];
      const distanceKm = Number(point.distance);
      return {
        provider: "ppl" as const,
        id: text(point.code) || text(point.externalId),
        name: text(point.name) || text(point.code),
        street: text(pointAddress.street),
        city: text(pointAddress.city),
        zip: text(pointAddress.zipCode),
        address: [
          text(pointAddress.street),
          text(pointAddress.zipCode),
          text(pointAddress.city),
        ]
          .filter(Boolean)
          .join(", "),
        isNonstop:
          regularHours.length === 7 &&
          regularHours.every(
            (day) =>
              day &&
              typeof day === "object" &&
              (day as Record<string, unknown>).nonstop === true,
          ),
        distanceKm: Number.isFinite(distanceKm) ? distanceKm : undefined,
      };
    })
    .filter((point) => point.id && point.address);
}

async function packetaSuggestions(
  address: string,
  city: string,
  postalCode: string,
  kindFilter?: string,
): Promise<Suggestion[]> {
  const apiKey =
    process.env.PACKETA_WIDGET_API_KEY?.trim() ||
    process.env.NEXT_PUBLIC_PACKETA_API_KEY?.trim() ||
    process.env.PACKETA_API_KEY?.trim();
  if (!apiKey) return [];

  const kinds =
    kindFilter === "zbox"
      ? ["box"]
      : kindFilter === "pickup_point"
        ? ["branch"]
        : ["branch", "box"];

  if (!packetaFeedCache || packetaFeedCache.expiresAt <= Date.now()) {
    packetaFeedRequest ??= Promise.all(
      ["branch", "box"].map(async (kind) => {
        const response = await fetch(
          `https://pickup-point.api.packeta.com/v5/${encodeURIComponent(apiKey)}/${kind}/json?lang=cs`,
          { signal: AbortSignal.timeout(15000) },
        );
        if (!response.ok) return [];
        const result = (await response.json()) as unknown;
        return Array.isArray(result)
          ? result.map((raw) => ({
              kind,
              point:
                raw && typeof raw === "object"
                  ? (raw as Record<string, unknown>)
                  : {},
            }))
          : [];
      }),
    )
      .then((feeds) => {
        packetaFeedCache = { expiresAt: Date.now() + 60 * 60 * 1000, feeds };
        return feeds;
      })
      .finally(() => {
        packetaFeedRequest = null;
      });
  }

  const feeds =
    packetaFeedCache && packetaFeedCache.expiresAt > Date.now()
      ? packetaFeedCache.feeds
      : packetaFeedRequest
        ? await packetaFeedRequest
        : [];

  const normalizedCity = normalize(city);
  const normalizedPostcode = postalCode.replace(/\D/g, "");
  const normalizedStreet = normalize(address.split(",")[0] || "");
  const matchingByKind = feeds
    .filter((feed) => kinds.includes(feed[0]?.kind) || feed.length === 0)
    .map((feed) =>
      feed
        .filter(({ kind, point }) => {
          if (kinds.length && !kinds.includes(kind)) return false;
          const status =
            point.status && typeof point.status === "object"
              ? (point.status as Record<string, unknown>)
              : {};
          const zipMatches =
            normalizedPostcode &&
            text(point.zip).replace(/\D/g, "") === normalizedPostcode;
          const cityMatches =
            normalizedCity &&
            normalize(
              `${text(point.city)} ${text(point.name)} ${text(point.place)}`,
            ).includes(normalizedCity);
          return (
            text(point.country).toLowerCase() === "cz" &&
            text(point.displayFrontend) === "1" &&
            (!text(status.statusId) || text(status.statusId) === "1") &&
            (zipMatches || cityMatches)
          );
        })
        .sort((a, b) => {
          const score = (point: Record<string, unknown>) => {
            const zipMatches =
              text(point.zip).replace(/\D/g, "") === normalizedPostcode;
            const streetMatches =
              normalizedStreet &&
              normalize(`${text(point.street)} ${text(point.name)}`).includes(
                normalizedStreet,
              );
            return Number(zipMatches) * 2 + Number(streetMatches);
          };
          return score(b.point) - score(a.point);
        })
        .map(({ kind, point }) => {
          const latitude = Number(point.latitude);
          const longitude = Number(point.longitude);
          return {
            provider: "packeta" as const,
            id: text(point.id),
            name: text(point.place) || text(point.name) || text(point.id),
            street: text(point.street),
            city: text(point.city),
            zip: text(point.zip),
            address: [text(point.street), text(point.zip), text(point.city)]
              .filter(Boolean)
              .join(", "),
            isNonstop:
              kind === "box" || text(point.type).toLowerCase() === "zbox",
            latitude: Number.isFinite(latitude) ? latitude : undefined,
            longitude: Number.isFinite(longitude) ? longitude : undefined,
          };
        })
        .filter((point) => point.id && point.address),
    );

  const suggestions: Suggestion[] = [];
  for (let index = 0; suggestions.length < 8; index += 1) {
    let added = false;
    for (const points of matchingByKind) {
      const point = points[index];
      if (point && !suggestions.some((candidate) => candidate.id === point.id)) {
        suggestions.push(point);
        added = true;
        if (suggestions.length === 8) break;
      }
    }
    if (!added) break;
  }
  return suggestions;
}

async function dpdSuggestions(address: string): Promise<Suggestion[]> {
  const url = new URL("https://pickup.dpd.cz/api/GetParcelShopsByAddress");
  url.searchParams.set("address", address);
  url.searchParams.set("limit", "8");
  url.searchParams.set("crossboarder", "0");
  const response = await fetch(url, { signal: AbortSignal.timeout(9000) });
  if (!response.ok) return [];
  const result = (await response.json()) as { data?: { items?: unknown[] } };
  return (result.data?.items ?? [])
    .slice(0, 8)
    .map((raw) => {
      const point =
        raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
      const street = [text(point.street), text(point.house_number)]
        .filter(Boolean)
        .join(" ");
      return {
        provider: "dpd" as const,
        id: text(point.id),
        name: text(point.company) || text(point.id),
        street,
        city: text(point.city),
        zip: text(point.postcode),
        address: [street, text(point.postcode), text(point.city)]
          .filter(Boolean)
          .join(", "),
        isNonstop: text(point.pickup_network_type) === "dpd_box",
      };
    })
    .filter((point) => point.id && point.address);
}

async function balikovnaSuggestions(
  city: string,
  postalCode: string,
): Promise<Suggestion[]> {
  const response = await fetch(
    "https://b2c.cpost.cz/locations/api/points?type=BALIKOVNY",
    { signal: AbortSignal.timeout(12000) },
  );
  if (!response.ok) return [];
  const points = (await response.json()) as unknown[];
  const normalizedCity = normalize(city);
  const normalizedPostcode = postalCode.replace(/\D/g, "");
  return points
    .map((raw) =>
      raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {},
    )
    .filter((point) => {
      const zipMatches =
        normalizedPostcode &&
        text(point.zip).replace(/\D/g, "") === normalizedPostcode;
      const cityMatches =
        normalizedCity &&
        normalize(
          `${text(point.municipality_name)} ${text(point.name)} ${text(point.address)}`,
        ).includes(normalizedCity);
      return zipMatches || cityMatches;
    })
    .sort(
      (a, b) =>
        Number(Boolean(text(b.zip).replace(/\D/g, "") === normalizedPostcode)) -
        Number(Boolean(text(a.zip).replace(/\D/g, "") === normalizedPostcode)),
    )
    .slice(0, 8)
    .map((point) => ({
      provider: "balikovna" as const,
      id: text(point.id),
      name: text(point.name) || text(point.id),
      street: text(point.address),
      city: text(point.municipality_name),
      zip: text(point.zip),
      address: text(point.address),
      isNonstop: text(point.subtype).toUpperCase().includes("BOX"),
    }))
    .filter((point) => point.id && point.address);
}

async function glsSuggestions(
  city: string,
  postalCode: string,
): Promise<Suggestion[]> {
  const response = await fetch(
    "https://ps-maps.gls-czech.com/getDropoffPoints.php?ctrcode=CZ",
    { signal: AbortSignal.timeout(12000) },
  );
  if (!response.ok) return [];
  const xml = await response.text();
  const normalizedCity = normalize(city);
  const normalizedPostcode = postalCode.replace(/\D/g, "");
  const points = [...xml.matchAll(/<DropoffPoint\s+([^>]+)>/g)].map((match) =>
    xmlAttributes(match[1]),
  );
  return points
    .filter((point) => {
      const zipMatches =
        normalizedPostcode && point.ZipCode?.replace(/\D/g, "") === normalizedPostcode;
      const cityMatches =
        normalizedCity &&
        normalize(
          `${point.CityName || ""} ${point.Name || ""} ${point.Address || ""}`,
        ).includes(normalizedCity);
      return zipMatches || cityMatches;
    })
    .sort(
      (a, b) =>
        Number(Boolean(b.ZipCode?.replace(/\D/g, "") === normalizedPostcode)) -
        Number(Boolean(a.ZipCode?.replace(/\D/g, "") === normalizedPostcode)),
    )
    .slice(0, 8)
    .map((point) => ({
      provider: "gls" as const,
      id: point.ID || "",
      name: point.Name || point.ID || "",
      street: point.Address || "",
      city: point.CityName || "",
      zip: point.ZipCode || "",
      address: [point.Address, point.ZipCode, point.CityName]
        .filter(Boolean)
        .join(", "),
      isNonstop: point.IsParcelLocker === "1",
    }))
    .filter((point) => point.id && point.address);
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<{ points: Suggestion[] }>,
) {
  if (req.method !== "GET") {
    return res.status(405).json({ points: [] });
  }

  const provider = String(req.query.provider || "");
  const address = String(req.query.address || "").trim().slice(0, 300);
  const city = String(req.query.city || "").trim().slice(0, 120);
  const postalCode = String(req.query.postalCode || "").trim().slice(0, 20);
  const kind = String(req.query.type || "").trim();

  if (
    !address ||
    !city ||
    !postalCode ||
    !["packeta", "ppl", "balikovna", "gls", "dpd"].includes(provider)
  ) {
    return res.status(200).json({ points: [] });
  }

  try {
    const points =
      provider === "packeta"
        ? await packetaSuggestions(address, city, postalCode, kind)
        : provider === "ppl"
          ? await pplSuggestions(address, requestOrigin(req))
          : provider === "dpd"
            ? await dpdSuggestions(address)
            : provider === "balikovna"
              ? await balikovnaSuggestions(city, postalCode)
              : await glsSuggestions(city, postalCode);

    res.setHeader(
      "Cache-Control",
      points.length > 0 ? "private, max-age=300" : "no-store",
    );
    return res.status(200).json({ points });
  } catch (error) {
    return res.status(200).json({ points: [] });
  }
}
