import type { LoaderFunctionArgs } from "react-router";
import { authenticate, unauthenticated } from "../shopify.server";
import prisma from "../db.server";

async function ensureAutomaticDiscount(params: {
  admin: any;
  dealId: string;
  shopifyDiscountId: string | null;
  productGid: string;
  discountType: string;
  discountValue: number;
  secondsRemaining: number;
}): Promise<string | null> {
  const { admin, dealId, shopifyDiscountId, productGid, discountType, discountValue, secondsRemaining } = params;
  if (!admin?.graphql) return null;

  const endsAt = new Date(Date.now() + Math.max(60, secondsRemaining) * 1000);

  // ── If we already have a stored GID, check if it's still active ───────────
  if (shopifyDiscountId) {
    try {
      const checkResp = await admin.graphql(`#graphql
        query CheckDiscount($id: ID!) {
          automaticDiscountNode(id: $id) {
            id
            automaticDiscount {
              ... on DiscountAutomaticBasic {
                status
              }
            }
          }
        }
      `, { variables: { id: shopifyDiscountId } });
      const checkJson = await checkResp.json();
      const node = checkJson?.data?.automaticDiscountNode;
      if (node?.automaticDiscount?.status === "ACTIVE") {
        return shopifyDiscountId; // still good — nothing to do
      }
      // Expired or gone — delete it so we can recreate with same title
      if (node) {
        await admin.graphql(`#graphql
          mutation DeleteDiscount($id: ID!) {
            discountAutomaticDelete(id: $id) {
              deletedAutomaticDiscountId
              userErrors { message }
            }
          }
        `, { variables: { id: shopifyDiscountId } });
      }
    } catch { /* best-effort */ }
  }

  // ── Create a fresh automatic discount ────────────────────────────────────
  const title = `Deal of the Hour — ${dealId}`;
  const automaticBasicDiscount: any = {
    title,
    startsAt: new Date().toISOString(),
    endsAt: endsAt.toISOString(),
    customerGets: {
      value:
        discountType === "fixed"
          ? { discountAmount: { amount: String(discountValue), appliesOnEachItem: true } }
          : { percentage: Number(discountValue) / 100 },
      items: { products: { productsToAdd: [productGid] } },
    },
  };

  try {
    const resp = await admin.graphql(`#graphql
      mutation discountAutomaticBasicCreate($automaticBasicDiscount: DiscountAutomaticBasicInput!) {
        discountAutomaticBasicCreate(automaticBasicDiscount: $automaticBasicDiscount) {
          automaticDiscountNode { id }
          userErrors { field code message }
        }
      }
    `, { variables: { automaticBasicDiscount } });
    const json = await resp.json();
    const errors = json?.data?.discountAutomaticBasicCreate?.userErrors;
    if (errors?.length) {
      // TAKEN means a discount with this title exists but we couldn't find/delete it.
      // Try deleting by searching title as a last resort.
      const taken = errors.find((e: any) => e.code === "TAKEN");
      if (taken) {
        await cleanupDiscountByTitle(admin, title);
        return null; // will retry on next widget load
      }
      console.error("[deal-discount] userErrors:", JSON.stringify(errors));
      return null;
    }
    const newId = json?.data?.discountAutomaticBasicCreate?.automaticDiscountNode?.id ?? null;
    return newId;
  } catch (e) {
    console.error("[deal-discount] mutation failed:", e);
    return null;
  }
}

// Last-resort cleanup: fetch all discounts and delete any matching the title by fetching larger page
async function cleanupDiscountByTitle(admin: any, title: string) {
  try {
    // Fetch a larger set and find by exact title
    const resp = await admin.graphql(`#graphql
      query FindAllDiscounts($query: String!) {
        automaticDiscountNodes(first: 50, query: $query) {
          nodes {
            id
            automaticDiscount {
              ... on DiscountAutomaticBasic { title status }
              ... on DiscountAutomaticApp { title status }
              ... on DiscountAutomaticBxgy { title status }
              ... on DiscountAutomaticFreeShipping { title status }
            }
          }
        }
      }
    `, { variables: { query: `title:'${title.split("—")[0].trim()}'` } });
    const json = await resp.json();
    const nodes: any[] = json?.data?.automaticDiscountNodes?.nodes ?? [];
    for (const node of nodes) {
      const nodeTitle = node.automaticDiscount?.title;
      if (nodeTitle === title) {
        await admin.graphql(`#graphql
          mutation DeleteDiscount($id: ID!) {
            discountAutomaticDelete(id: $id) { deletedAutomaticDiscountId userErrors { message } }
          }
        `, { variables: { id: node.id } });
      }
    }
  } catch { /* best-effort */ }
}

// ── Coming-soon deal finder ───────────────────────────────────────────────────
// Finds the earliest upcoming deal within maxHours from now.
// Returns the deal info with startsInSeconds, or null if nothing is close enough.

interface ComingSoonInfo {
  startsInSeconds: number;
  productTitle: string | null;
  productImage: string | null;
  startHour: number;
  startMinute: number;
  startDateLabel: string;   // "Today", "Tomorrow", or "Mon, Jul 8"
  discountType: string;
  discountValue: number;
  productNumericId: string;
}

async function findNextUpcomingDeal(
  shop: string,
  now: Date,
  currentParts: { hour: number; minute: number; second: number; dateKey: string; dayOfWeek: number },
  timezone: string,
  maxHours: number
): Promise<ComingSoonInfo | null> {
  const { hour: nowHour, minute: nowMinute, second: nowSecond, dateKey: todayKey, dayOfWeek: todayDow } = currentParts;
  const nowMinutes = nowHour * 60 + nowMinute;
  const minutesLeftToday = 24 * 60 - nowMinutes;
  const maxMinutes = maxHours * 60;

  // Determine how many future days we need (0 = today only, 1 = today + tomorrow, etc.)
  const extraDaysNeeded = Math.max(0, Math.ceil((maxMinutes - minutesLeftToday) / (24 * 60)));

  // Build date info for each day we need to check
  const days: Array<{ dateKey: string; dayOfWeek: number; dayOffset: number }> = [
    { dateKey: todayKey, dayOfWeek: todayDow, dayOffset: 0 },
  ];
  for (let d = 1; d <= extraDaysNeeded; d++) {
    const futureDate = new Date(now.getTime() + d * 24 * 60 * 60 * 1000);
    const parts = getTzParts(futureDate, timezone);
    days.push({ dateKey: parts.dateKey, dayOfWeek: parts.dayOfWeek, dayOffset: d });
  }

  // Build the OR query conditions
  // Day 0 (today): only deals starting strictly after now
  // Day d > 0: all deals on that day
  const orConds: any[] = [
    { specificDate: todayKey, startHour: { gt: nowHour } },
    { specificDate: todayKey, startHour: nowHour, startMinute: { gt: nowMinute } },
    { specificDate: null, dayOfWeek: todayDow, startHour: { gt: nowHour } },
    { specificDate: null, dayOfWeek: todayDow, startHour: nowHour, startMinute: { gt: nowMinute } },
  ];
  for (const { dateKey, dayOfWeek, dayOffset } of days) {
    if (dayOffset === 0) continue; // already added today above
    orConds.push({ specificDate: dateKey });
    orConds.push({ specificDate: null, dayOfWeek });
  }

  const candidates = await prisma.dealSlot.findMany({
    where: { shop, isActive: true, OR: orConds },
    orderBy: [{ startHour: "asc" }, { startMinute: "asc" }],
    take: 30,
  });

  let best: { startsInSeconds: number; deal: typeof candidates[number]; dayOffset: number } | null = null;

  for (const deal of candidates) {
    const dealMins = deal.startHour * 60 + deal.startMinute;
    let startsInSeconds: number | null = null;

    // Walk through days in order to find the first matching day for this deal
    let matchedDayOffset = -1;
    for (const { dateKey, dayOfWeek, dayOffset } of days) {
      const matchesDate = deal.specificDate === dateKey;
      const matchesDow  = deal.specificDate === null && deal.dayOfWeek === dayOfWeek;
      if (!matchesDate && !matchesDow) continue;

      if (dayOffset === 0) {
        // Today — must start strictly after now
        if (dealMins <= nowMinutes) continue;
        startsInSeconds = (dealMins - nowMinutes) * 60 - nowSecond;
      } else {
        // Future day — compute minutes from now until midnight tonight + deal start minutes
        const minutesToMidnight = minutesLeftToday;
        const minutesIntoFutureDay = dayOffset > 1
          ? (dayOffset - 1) * 24 * 60 + dealMins
          : dealMins;
        startsInSeconds = (minutesToMidnight + minutesIntoFutureDay) * 60 - nowSecond;
      }
      matchedDayOffset = dayOffset;
      break; // use first (earliest) matching day
    }

    if (startsInSeconds === null || startsInSeconds <= 0 || matchedDayOffset < 0) continue;
    if (startsInSeconds > maxHours * 3600) continue;

    if (!best || startsInSeconds < best.startsInSeconds) {
      best = { startsInSeconds: Math.round(startsInSeconds), deal, dayOffset: matchedDayOffset };
    }
  }

  if (!best) return null;

  // Build a human-readable date label so the widget can show "Today", "Tomorrow", or a date
  let startDateLabel = "Today";
  if (best.dayOffset === 1) {
    startDateLabel = "Tomorrow";
  } else if (best.dayOffset > 1) {
    const dealDate = new Date(now.getTime() + best.startsInSeconds * 1000);
    startDateLabel = new Intl.DateTimeFormat("en-US", {
      weekday: "short",
      month: "short",
      day: "numeric",
      timeZone: timezone,
    }).format(dealDate);
  }

  return {
    startsInSeconds: best.startsInSeconds,
    productTitle: best.deal.productTitle || null,
    productImage: best.deal.productImage || null,
    startHour: best.deal.startHour,
    startMinute: best.deal.startMinute,
    startDateLabel,
    discountType: best.deal.discountType,
    discountValue: best.deal.discountValue,
    productNumericId: best.deal.productId.replace("gid://shopify/Product/", ""),
  };
}

// ── Reliable timezone helpers (no locale-string re-parse trick) ───────────────

function getTzParts(date: Date, tz: string) {
  const fmt = new Intl.DateTimeFormat("en-US", {
    timeZone: tz,
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hourCycle: "h23",
    weekday: "short",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
  const parts = fmt.formatToParts(date);
  const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "0";
  const dow = ({ Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 } as Record<string, number>)[get("weekday")] ?? 0;
  return {
    dayOfWeek: dow,
    hour: parseInt(get("hour"), 10),
    minute: parseInt(get("minute"), 10),
    second: parseInt(get("second"), 10),
    dateKey: `${get("year")}-${get("month")}-${get("day")}`,
  };
}

export const loader = async ({ request }: LoaderFunctionArgs) => {
  // Verify the Shopify App Proxy HMAC signature — prevents spoofed requests
  const { session } = await authenticate.public.appProxy(request);
  const shop = session.shop;

  const url = new URL(request.url);
  const settings = await prisma.appSettings.findUnique({ where: { shop } });
  const timezone = settings?.timezone ?? "UTC";

  // Get current time correctly in shop's timezone
  const now = new Date();
  const { dayOfWeek, hour: currentHour, minute: currentMinute, second: currentSecond, dateKey } = getTzParts(now, timezone);

  // Debug: add ?debug=1 to the URL to see what the server computes
  if (url.searchParams.get("debug") === "1") {
    return Response.json({
      serverUtcNow: now.toISOString(),
      timezone,
      computed: { dayOfWeek, currentHour, currentMinute, dateKey },
    }, { headers: { "Access-Control-Allow-Origin": "*" } });
  }

  // Find the currently active deal slot — date-specific deals take priority over recurring.
  // Fetch multiple candidates and pick the first one that's still within its time window.
  // (findFirst alone isn't enough — the top-ranked deal may have already expired.)
  const candidates = await prisma.dealSlot.findMany({
    where: {
      shop,
      isActive: true,
      startHour: { lte: currentHour },
      OR: [
        { specificDate: dateKey },                 // one-time deal for today
        { specificDate: null, dayOfWeek },         // legacy recurring deal
      ],
    },
    orderBy: [
      { specificDate: "desc" },  // prefer date-specific over recurring
      { startHour: "desc" },
    ],
    take: 10,
  });

  const nowMinutes = currentHour * 60 + currentMinute;

  // Walk candidates in priority order and return the first one still within its window
  const currentDeal = candidates.find((deal) => {
    const start = deal.startHour * 60 + deal.startMinute;
    const end = start + deal.durationMinutes;
    return nowMinutes >= start && nowMinutes < end;
  }) ?? null;

  if (!currentDeal) {
    const widgetHideHours = settings?.widgetHideHours ?? 24;
    const comingSoon = await findNextUpcomingDeal(
      shop,
      now,
      { hour: currentHour, minute: currentMinute, second: currentSecond, dateKey, dayOfWeek },
      timezone,
      widgetHideHours
    );
    return Response.json(
      {
        deal: null,
        nextDeals: [],
        widgetVisible: comingSoon !== null,
        comingSoon: comingSoon ?? null,
      },
      { headers: { "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*" } }
    );
  }

  const dealStartMinutes = currentDeal.startHour * 60 + currentDeal.startMinute;
  const dealEndMinutes = dealStartMinutes + currentDeal.durationMinutes;

  // Calculate time remaining in seconds
  const secondsRemaining = (dealEndMinutes - nowMinutes) * 60 - currentSecond;

  let hydratedOriginalPrice = currentDeal.originalPrice;
  let hydratedProductImage = currentDeal.productImage;
  let hydratedProductSku = currentDeal.productSku;
  let hydratedProductTitle = currentDeal.productTitle;
  let hydratedProductHandle: string | null = null;
  let hydratedVariantId: string | null = null;
  let hydratedInventoryQuantity: number | null = null;
  let hydratedDescriptionHtml: string | null = null;

  // Single admin client shared across all three API call blocks below
  const { admin } = await unauthenticated.admin(shop);

  // Always fetch live product data (handle, variantId, inventory) + hydrate cached fields if missing.
  try {
    const query = `#graphql
      query DealProduct($id: ID!) {
        product(id: $id) {
          handle
          title
          descriptionHtml
          featuredImage { url }
          variants(first: 1) {
            nodes {
              sku
              price
              legacyResourceId
              inventoryQuantity
            }
          }
        }
      }
    `;

    const resp = await admin.graphql(query, { variables: { id: currentDeal.productId } });
    const json = await resp.json();
    const product = json?.data?.product;
    const variant = product?.variants?.nodes?.[0];

    if (product?.handle) hydratedProductHandle = product.handle;
    if (variant?.legacyResourceId != null) hydratedVariantId = String(variant.legacyResourceId);
    // Always capture live inventory
    if (variant?.inventoryQuantity != null) hydratedInventoryQuantity = Number(variant.inventoryQuantity);
    if (product?.descriptionHtml) hydratedDescriptionHtml = product.descriptionHtml;

    const price = variant?.price != null ? Number(variant.price) : null;
    if (price != null && !Number.isNaN(price) && price > 0) hydratedOriginalPrice = price;
    if (product?.featuredImage?.url) hydratedProductImage = product.featuredImage.url;
    if (variant?.sku) hydratedProductSku = variant.sku;
    if (product?.title) hydratedProductTitle = product.title;

    // Persist product fields to DB if not yet stored
    if (hydratedOriginalPrice > 0 && (!currentDeal.originalPrice || currentDeal.originalPrice <= 0)) {
      await prisma.dealSlot.update({
        where: { id: currentDeal.id },
        data: {
          originalPrice: hydratedOriginalPrice,
          productImage: hydratedProductImage,
          productSku: hydratedProductSku,
          productTitle: hydratedProductTitle,
        },
      });
    }
  } catch { }

  // Compute discounted price
  let discountedPrice = hydratedOriginalPrice;
  if (currentDeal.discountType === "percentage") {
    discountedPrice = hydratedOriginalPrice * (1 - currentDeal.discountValue / 100);
  } else {
    discountedPrice = hydratedOriginalPrice - currentDeal.discountValue;
  }

  // Extract numeric id (fallback) for storefront link building
  const numericProductId = currentDeal.productId.replace("gid://shopify/Product/", "");

  // Get analytics for today's run of this deal
  let unitsSold = 0;
  let initialInventory: number | null = null;
  try {
    const analytics = await prisma.dealAnalytics.findFirst({
      where: {
        shop,
        dealSlotId: currentDeal.id,
        runDate: { gte: new Date(`${dateKey}T00:00:00.000Z`) },
      },
    });
    unitsSold = analytics?.unitsSold ?? 0;
    initialInventory = analytics?.initialInventory ?? null;
  } catch (e) { }

  // ── Promotion quantity logic ─────────────────────────────────────────────────
  // If a promotionQuantity cap is set, the deal ends as soon as that many units
  // have been sold — even if the time slot hasn't expired yet.
  const promoQty = currentDeal.promotionQuantity ?? null; // null = no cap

  if (promoQty !== null && unitsSold >= promoQty) {
    // Promo stock exhausted — treat as no active deal (triggers next deal on widget)
    return Response.json(
      { deal: null, nextDeals: [] },
      { headers: { "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*" } }
    );
  }

  // When a promo cap is in place, show promo figures in the inventory bar,
  // not the raw Shopify stock figures.
  const effectiveInventory = promoQty !== null
    ? Math.max(0, promoQty - unitsSold)   // units remaining in this promotion
    : hydratedInventoryQuantity;

  const effectiveInitialInventory = promoQty !== null
    ? promoQty                             // promo total is the reference, not real stock
    : initialInventory ?? hydratedInventoryQuantity;

  // Upcoming deals — deals that start after the current deal ends.
  // Two cases:
  //   1. Same hour but a later startMinute (sub-hour deals in the same slot)
  //   2. Any deal in a future hour
  const currentDealEndMinute = currentDeal.startMinute + currentDeal.durationMinutes;
  const upcoming = await prisma.dealSlot.findMany({
    where: {
      shop,
      isActive: true,
      id: { not: currentDeal.id }, // exclude the running deal itself
      OR: [
        // Sub-hour: same hour, starts at or after current deal ends
        {
          startHour: currentHour,
          startMinute: { gte: currentDealEndMinute },
          OR: [
            { specificDate: dateKey },
            { specificDate: null, dayOfWeek },
          ],
        },
        // Future hours
        {
          startHour: { gt: currentHour },
          OR: [
            { specificDate: dateKey },
            { specificDate: null, dayOfWeek },
          ],
        },
      ],
    },
    orderBy: [{ startHour: "asc" }, { startMinute: "asc" }],
    take: 15,
  });

  // Hydrate productImage / productTitle / handle for upcoming deals
  const upcomingImages: Record<string, string | null> = {};
  const upcomingHandles: Record<string, string> = {};

  // Always fetch handles for all upcoming deals (handle is never stored in DB).
  // Also fetch image/title for deals that are missing them.
  const needsData = upcoming.filter((d) => d.productId);
  if (needsData.length > 0) {
    try {
      const upAdmin = admin;
      const aliases = needsData
        .map((d, i) => `p${i}: product(id: "${d.productId}") { handle featuredImage { url } title }`)
        .join("\n");
      const batchQuery = `#graphql\n{ ${aliases} }`;
      const batchResp = await upAdmin.graphql(batchQuery);
      const batchJson = await batchResp.json();
      needsData.forEach((d, i) => {
        const p = batchJson?.data?.[`p${i}`];
        if (p?.handle) upcomingHandles[d.id] = p.handle;
        if (p?.featuredImage?.url && !d.productImage) upcomingImages[d.id] = p.featuredImage.url;
        // Persist missing image/title to DB so future requests are faster
        if (p && (!d.productImage || !d.productTitle)) {
          prisma.dealSlot.update({
            where: { id: d.id },
            data: {
              ...(!d.productImage && p.featuredImage?.url ? { productImage: p.featuredImage.url } : {}),
              ...(!d.productTitle && p.title ? { productTitle: p.title } : {}),
            },
          }).catch(() => { });
        }
      });
    } catch { }
  }

  // Track impression + snapshot initialInventory on first load of the day
  try {
    const analyticsId = `${currentDeal.id}_${dateKey}`;
    const runDate = new Date(`${dateKey}T00:00:00.000Z`);
    const existing = await prisma.dealAnalytics.findUnique({ where: { id: analyticsId } });
    if (!existing) {
      await prisma.dealAnalytics.create({
        data: {
          id: analyticsId,
          shop,
          dealSlotId: currentDeal.id,
          runDate,
          impressions: 1,
          // Snapshot current inventory as the starting point for this deal run
          initialInventory: hydratedInventoryQuantity ?? undefined,
        },
      });
      // Use current inventory as initialInventory for this response
      initialInventory = hydratedInventoryQuantity;
    } else {
      await prisma.dealAnalytics.update({
        where: { id: analyticsId },
        data: { impressions: { increment: 1 } },
      });
      // If initialInventory was never stored (old records), backfill it now
      if (existing.initialInventory == null && hydratedInventoryQuantity != null) {
        await prisma.dealAnalytics.update({
          where: { id: analyticsId },
          data: { initialInventory: hydratedInventoryQuantity },
        });
        initialInventory = hydratedInventoryQuantity;
      }
    }
  } catch (e) { }

  // Ensure a real Shopify automatic discount exists so cart/checkout reflect the discounted price.
  // This runs best-effort; failures should not break storefront rendering.
  try {
    const newDiscountId = await ensureAutomaticDiscount({
      admin,
      dealId: currentDeal.id,
      shopifyDiscountId: currentDeal.shopifyDiscountId ?? null,
      productGid: currentDeal.productId,
      discountType: currentDeal.discountType,
      discountValue: currentDeal.discountValue,
      secondsRemaining,
    });
    // Persist the new discount GID so future requests skip the search
    if (newDiscountId && newDiscountId !== currentDeal.shopifyDiscountId) {
      await prisma.dealSlot.update({
        where: { id: currentDeal.id },
        data: { shopifyDiscountId: newDiscountId },
      });
    }
  } catch (e) {
    console.error("[deal-discount] unauthenticated.admin failed:", e);
  }

  return Response.json(
    {
      deal: {
        id: currentDeal.id,
        productId: currentDeal.productId,
        productNumericId: numericProductId,
        productTitle: hydratedProductTitle,
        productSku: hydratedProductSku,
        productImage: hydratedProductImage,
        productHandle: hydratedProductHandle,
        productDescriptionHtml: hydratedDescriptionHtml,
        variantId: hydratedVariantId,
        originalPrice: hydratedOriginalPrice,
        discountedPrice: Math.max(0, discountedPrice),
        discountType: currentDeal.discountType,
        discountValue: currentDeal.discountValue,
        secondsRemaining,
        unitsSold,
        inventoryQuantity: effectiveInventory,
        initialInventory: effectiveInitialInventory,
        promotionQuantity: promoQty,        // lets the widget know a cap is in place
      },
      nextDeals: upcoming.map((d) => ({
        productTitle: d.productTitle,
        productImage: d.productImage || upcomingImages[d.id] || null,
        productHandle: upcomingHandles[d.id] || null,
        productNumericId: d.productId.replace("gid://shopify/Product/", ""),
        startHour: d.startHour,
        startMinute: d.startMinute,
        discountType: d.discountType,
        discountValue: d.discountValue,
      })),
      showNextDeals: settings?.showNextDeals ?? true,
      widgetVisible: true,
      comingSoon: null,
      // widgetTitle/widgetSubtitle removed — theme block settings are the source of truth
    },
    {
      headers: {
        "Cache-Control": "no-store",
        "Access-Control-Allow-Origin": "*",
      },
    }
  );
};

