type Env = {
  ALLOWED_ORIGIN?: string;
  CTT_OBJECT_SEARCH_URL?: string;
  CTT_PUBLIC_AREA_URL?: string;
};

type TrackingUpdate = {
  date?: string;
  time?: string;
  status?: string;
  location?: string;
  details?: string;
};

type TrackingResponse = {
  provider: "ctt";
  code: string;
  sourceUrl: string;
  fetchedAt: string;
  status?: string;
  summary?: string;
  updates: TrackingUpdate[];
  rawText?: string;
};

const DEFAULT_OBJECT_SEARCH_URL =
  "https://appserver2.ctt.pt/feapl_2/app/open/objectSearch/objectSearch.jspx?request_locale=pt";
const DEFAULT_PUBLIC_AREA_URL = "https://appserver.ctt.pt/CustomerArea/PublicArea";
const MAX_CODE_LENGTH = 40;

const COMMON_STATUS_WORDS = [
  "Aceite",
  "Aguarda entrada nos CTT",
  "Em exportação",
  "Em importação",
  "Em trânsito",
  "Em espera",
  "Entrega não conseguida",
  "Entregue",
  "Distribuição",
  "Expedido",
  "Não entregue",
  "Reexpedido",
];

function corsHeaders(request: Request, env: Env) {
  const requestOrigin = request.headers.get("origin") || "";
  const allowedOrigin = env.ALLOWED_ORIGIN || requestOrigin || "*";

  return {
    "access-control-allow-origin": allowedOrigin,
    "access-control-allow-methods": "GET,POST,OPTIONS",
    "access-control-allow-headers": request.headers.get("access-control-request-headers") || "content-type",
    "access-control-max-age": "86400",
    vary: "origin",
  };
}

function withCors(request: Request, env: Env, response: Response) {
  const headers = new Headers(response.headers);
  const cors = corsHeaders(request, env);
  for (const [key, value] of Object.entries(cors)) {
    headers.set(key, value);
  }

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  });
}

function optionsResponse(request: Request, env: Env) {
  return new Response(null, { status: 204, headers: corsHeaders(request, env) });
}

function jsonResponse(data: unknown, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
    },
  });
}

function normalizeCode(raw: string) {
  return String(raw || "")
    .normalize("NFKC")
    .replace(/\s+/g, "")
    .toUpperCase()
    .slice(0, MAX_CODE_LENGTH);
}

function isValidTrackingCode(code: string) {
  return /^[A-Z0-9]{6,40}$/.test(code);
}

function browserHeaders(referer: string) {
  return {
    "user-agent":
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
    accept:
      "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "accept-language": "pt-PT,pt;q=0.9,en-US;q=0.8,en;q=0.7",
    "cache-control": "no-cache",
    pragma: "no-cache",
    "sec-ch-ua": '"Chromium";v="125", "Not.A/Brand";v="24"',
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": '"Windows"',
    "sec-fetch-dest": "document",
    "sec-fetch-mode": "navigate",
    "sec-fetch-site": "none",
    "sec-fetch-user": "?1",
    "upgrade-insecure-requests": "1",
    referer,
  };
}

async function fetchText(url: string, init?: RequestInit) {
  const response = await fetch(url, init);
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`CTT request failed with status ${response.status} for ${url}`);
  }
  return { response, text };
}

function stripHtml(input: string) {
  return String(input || "")
    .replace(/<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<style[\s\S]*?<\/style>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/&nbsp;/gi, " ")
    .replace(/&amp;/gi, "&")
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/\s+/g, " ")
    .trim();
}

function pickFirstMatch(text: string, patterns: RegExp[]) {
  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match?.[1]) return match[1].trim();
  }
  return undefined;
}

function detectOverallStatus(text: string) {
  for (const status of COMMON_STATUS_WORDS) {
    if (text.includes(status)) return status;
  }
  return undefined;
}

function splitCandidateLines(text: string) {
  return text
    .split(/(?:\r?\n|\s{2,}|\u2022|\|)/g)
    .map((part) => part.replace(/\s+/g, " ").trim())
    .filter((part) => part.length > 0)
    .filter((part) => part.length > 3);
}

function parseUpdatesFromText(text: string): TrackingUpdate[] {
  const lines = splitCandidateLines(text);
  const updates: TrackingUpdate[] = [];

  const dateTimePattern = /(\d{1,2}\/\d{1,2}\/\d{2,4})\s+(\d{1,2}:\d{2})/;
  const dateOnlyPattern = /(\d{1,2}\/\d{1,2}\/\d{2,4})/;

  for (let index = 0; index < lines.length; index++) {
    const line = lines[index];
    const hasDateTime = dateTimePattern.test(line);
    const hasStatusWord = COMMON_STATUS_WORDS.some((status) => line.includes(status));

    if (!hasDateTime && !hasStatusWord) {
      continue;
    }

    const dateTime = line.match(dateTimePattern);
    const dateOnly = line.match(dateOnlyPattern);
    const date = dateTime?.[1] || dateOnly?.[1];
    const time = dateTime?.[2];

    const details = [line, lines[index + 1], lines[index + 2]]
      .filter(Boolean)
      .join(" ")
      .trim();

    updates.push({
      date,
      time,
      status: detectOverallStatus(line) || detectOverallStatus(details),
      details,
    });
  }

  return updates.slice(0, 20);
}

function buildSearchPayload(code: string, recaptchaToken?: string) {
  const params = new URLSearchParams();
  params.set("objects", code);
  if (recaptchaToken) {
    params.set("g-recaptcha-response", recaptchaToken);
  }
  return params;
}

async function tryTrackingRequest(url: string, code: string, recaptchaToken?: string) {
  const payload = buildSearchPayload(code, recaptchaToken);

  const attempts: Array<{ method: "POST" | "GET"; url: string; body?: BodyInit }> = [
    { method: "POST", url, body: payload },
    { method: "GET", url: `${url}${url.includes("?") ? "&" : "?"}${payload.toString()}` },
  ];

  for (const attempt of attempts) {
    const headers: Record<string, string> = browserHeaders(url);
    if (attempt.method === "POST") {
      headers["content-type"] = "application/x-www-form-urlencoded; charset=UTF-8";
    }

    try {
      const { text } = await fetchText(attempt.url, {
        method: attempt.method,
        headers,
        body: attempt.body,
        redirect: "follow",
      });
      const plainText = stripHtml(text);
      if (plainText.length > 20) {
        return { html: text, plainText };
      }
    } catch {
      // Try the next variant.
    }
  }

  throw new Error("CTT tracking request failed for all attempt variants");
}

async function trackObject(env: Env, code: string, recaptchaToken?: string): Promise<TrackingResponse> {
  const objectSearchUrl = env.CTT_OBJECT_SEARCH_URL || DEFAULT_OBJECT_SEARCH_URL;
  const publicAreaUrl = env.CTT_PUBLIC_AREA_URL || DEFAULT_PUBLIC_AREA_URL;

  const candidates = [publicAreaUrl, objectSearchUrl];
  let lastError: unknown = null;

  for (const candidate of candidates) {
    try {
      const result = await tryTrackingRequest(candidate, code, recaptchaToken);
      const summary = pickFirstMatch(result.plainText, [
        /(?:Estado|Situação)\s*[:\-]?\s*([^\n\r]+)/i,
        /(?:Tracking|Rastreio)\s*[:\-]?\s*([^\n\r]+)/i,
      ]);
      const status = summary || detectOverallStatus(result.plainText);
      const updates = parseUpdatesFromText(result.plainText);

      return {
        provider: "ctt",
        code,
        sourceUrl: candidate,
        fetchedAt: new Date().toISOString(),
        status,
        summary,
        updates,
        rawText: result.plainText.slice(0, 6000),
      };
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError instanceof Error ? lastError : new Error("Unable to fetch CTT tracking data");
}

async function parseRequestBody(request: Request) {
  const contentType = request.headers.get("content-type") || "";

  if (contentType.includes("application/json")) {
    return (await request.json().catch(() => ({}))) as Record<string, unknown>;
  }

  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
    const formData = await request.formData().catch(() => null);
    if (!formData) return {};
    return Object.fromEntries(formData.entries()) as Record<string, unknown>;
  }

  return {};
}

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    if (request.method === "OPTIONS") {
      return optionsResponse(request, env);
    }

    if (url.pathname === "/api/health") {
      return withCors(request, env, jsonResponse({ ok: true, service: "ctt-tracking" }));
    }

    if (url.pathname !== "/api/tracking") {
      return withCors(request, env, jsonResponse({ error: "Not found" }, 404));
    }

    if (request.method !== "GET" && request.method !== "POST") {
      return withCors(request, env, jsonResponse({ error: "Method not allowed" }, 405));
    }

    try {
      const body = request.method === "POST" ? await parseRequestBody(request) : {};
      const rawCode = String(body.code || url.searchParams.get("code") || "");
      const recaptchaToken = String(body.recaptchaToken || body["g-recaptcha-response"] || url.searchParams.get("recaptchaToken") || "").trim();
      const code = normalizeCode(rawCode);

      if (!code) {
        return withCors(request, env, jsonResponse({ error: "Missing tracking code" }, 400));
      }

      if (!isValidTrackingCode(code)) {
        return withCors(request, env, jsonResponse({ error: "Invalid tracking code" }, 400));
      }

      const result = await trackObject(env, code, recaptchaToken || undefined);
      return withCors(request, env, jsonResponse(result));
    } catch (error) {
      return withCors(
        request,
        env,
        jsonResponse(
          {
            error: "Upstream error",
            message: error instanceof Error ? error.message : String(error),
          },
          502,
        ),
      );
    }
  },
};
