import { authenticate } from "../shopify.server";
import {
  recordDiscountRejectionEvent,
  verifyIngestSecret,
} from "../models.discount-rejection.server";
import { evaluateDiscountRejectionThreshold } from "../utils.discount-rejection-notifier.server";
import { describeAuthFailure } from "../utils.checkout-auth.server";
import { overRateLimit } from "../utils.rate-limit.server";

/** Header the sandboxed web pixel presents, carrying the shop's secret. */
const PIXEL_KEY_HEADER = "X-Discount-Rejection-Key";

/**
 * The shopper's IP, taken from the request rather than the body.
 *
 * This endpoint is public, so the header cannot be trusted as sent. Apache sits
 * in front and *appends* the connecting peer to any X-Forwarded-For the caller
 * supplied, so the last entry is the one Apache observed and the only one a
 * caller cannot forge; everything before it is attacker-controlled. Taking the
 * first entry, the usual idiom, lets anyone write a false IP into the log.
 *
 * This holds only while exactly one trusted proxy sits in front. If a CDN is
 * ever put ahead of Apache, the real client moves one position earlier and this
 * needs revisiting.
 */
function getClientIp(request) {
  const forwarded = request.headers.get("X-Forwarded-For") || "";
  const hops = forwarded
    .split(",")
    .map((value) => value.trim())
    .filter(Boolean);

  return hops[hops.length - 1] || request.headers.get("X-Real-IP") || null;
}

/**
 * Per-shop ingest budget. Generous enough that a busy store reporting real
 * rejections is never throttled, tight enough that a flood cannot fill the
 * table or trip the Slack threshold.
 */
const RATE_LIMIT_MAX = 120;

/**
 * Establishes which shop this report is for, and that the caller may write to
 * it. Two callers, two credentials:
 *
 *  - the checkout UI extension can fetch a session token, which Shopify signs;
 *    the shop comes from the token, never from the body.
 *  - the web pixel runs in a sandbox with no session token, so it presents the
 *    shop's ingest secret instead. Knowing that secret is what authorises the
 *    shop named in the body.
 *
 * @returns {Promise<{shop: string} | {error: string, status: number}>}
 */
async function authorizeReport(request, payload) {
  if (request.headers.get("Authorization")) {
    try {
      const { sessionToken } = await authenticate.public.checkout(request);
      // `dest` is the shop the token was minted for, e.g. "shop.myshopify.com".
      const shop = String(sessionToken?.dest ?? "").replace(/^https:\/\//, "");

      if (!shop) return { error: "Session token carried no shop.", status: 401 };

      return { shop };
    } catch (error) {
      const [body, init] = describeAuthFailure(error);

      return { error: body.error, status: init.status };
    }
  }

  const shop = typeof payload?.shop === "string" ? payload.shop.trim() : "";
  const key = request.headers.get(PIXEL_KEY_HEADER);

  if (!shop) return { error: "shop is required.", status: 400 };

  if (!(await verifyIngestSecret(shop, key))) {
    return { error: "Unauthorized.", status: 401 };
  }

  return { shop };
}

/**
 * Receives rejected discount attempts from the discount rejection monitor
 * checkout extension. Always answers 200-with-ok quickly: checkout must never
 * be affected by monitoring.
 */

/**
 * Any origin may attempt a report; the credential decides whether it counts.
 *
 * A strict-runtime web pixel posts from a sandboxed context whose Origin can be
 * `null` or an opaque value, so echoing a fixed allowlisted origin back would
 * fail the browser's CORS check and the report would never arrive. Allowing any
 * origin costs nothing here: the endpoint carries no cookies and no ambient
 * authority, so a reply reveals nothing a caller could not already see, and
 * writing still requires a session token or the shop's ingest secret.
 */
function getCorsHeaders() {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": `Content-Type, Authorization, ${PIXEL_KEY_HEADER}`,
    "Access-Control-Max-Age": "86400",
    "Cache-Control": "no-store",
  };
}

function jsonWithCors(body, init) {
  const response = Response.json(body, init);

  Object.entries(getCorsHeaders()).forEach(([key, value]) =>
    response.headers.set(key, value),
  );

  return response;
}

export const loader = async ({ request }) => {
  if (request.method === "OPTIONS") {
    return new Response(null, { status: 204, headers: getCorsHeaders() });
  }

  return jsonWithCors({ error: "Method not allowed" }, { status: 405 });
};

export const action = async ({ request }) => {
  if (request.method === "OPTIONS") {
    return new Response(null, { status: 204, headers: getCorsHeaders() });
  }

  if (request.method !== "POST") {
    return jsonWithCors({ error: "Method not allowed" }, { status: 405 });
  }

  let payload;

  try {
    payload = await request.json();
  } catch {
    return jsonWithCors({ error: "Invalid JSON body" }, { status: 400 });
  }

  const auth = await authorizeReport(request, payload);

  if (auth.error) {
    return jsonWithCors({ error: auth.error }, { status: auth.status });
  }

  const { shop } = auth;
  const discountCode =
    typeof payload?.discountCode === "string" ? payload.discountCode.trim() : "";

  if (!discountCode) {
    return jsonWithCors(
      { error: "discountCode is required." },
      { status: 400 },
    );
  }

  if (overRateLimit(`rejection:${shop}`, RATE_LIMIT_MAX)) {
    return jsonWithCors(
      { ok: false, error: "Too many reports; try again shortly." },
      { status: 429 },
    );
  }

  try {
    const event = await recordDiscountRejectionEvent({
      shop,
      discountCode,
      customerId: payload?.customerId,
      customerEmail: payload?.customerEmail,
      customerName: payload?.customerName,
      checkoutToken: payload?.checkoutToken,
      clientId: payload?.clientId,
      // From the request, not the body: present on every row and unspoofable.
      ipAddress: getClientIp(request),
      userAgent: request.headers.get("User-Agent"),
      source: payload?.source,
      surface: payload?.surface,
      rejectionReason: payload?.rejectionReason,
      status: payload?.status,
      checkoutData: payload?.checkoutData,
    });

    // Only an unusable payload (no shop or no code) gets here now: a missing
    // customer id or email never prevents logging.
    if (!event) {
      return jsonWithCors(
        { ok: false, error: "shop and discountCode are required." },
        { status: 400 },
      );
    }

    const result = await evaluateDiscountRejectionThreshold({
      shop,
      discountCode,
      storeName: payload?.storeName,
    });

    // Counts are safe to return; nothing about the Slack webhook is exposed.
    return jsonWithCors(
      { ok: true, uniqueCustomerCount: result.uniqueCustomerCount ?? null },
      {},
    );
  } catch (error) {
    console.error("[discount-rejection] failed to record event", error);

    return jsonWithCors(
      { ok: false, error: "Unable to record discount rejection." },
      { status: 500 },
    );
  }
};
