import fs from "fs";
import path from "path";
import type { ImageSlot } from "./mpc-types";
import { uploadedPublicFilePath } from "./runtime-storage";

export function slotSafeName(value: string) {
  return String(value || "image")
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80) || "image";
}

export function isAdminUploadUrl(value?: string) {
  const text = String(value || "");
  return text.startsWith("/uploads/admin/") && !text.includes("..") && !text.startsWith("//");
}

export function adminUploadFileExists(value?: string) {
  if (!isAdminUploadUrl(value)) return true;
  try {
    // Production uploads are deliberately stored outside the deployable app
    // package. Checking only public/uploads made every valid admin upload look
    // missing to Next.js after sign-out, so the image slot was reset to its
    // packaged fallback. Check the isolated runtime file first.
    const runtimeFile = uploadedPublicFilePath(String(value || ""));
    if (runtimeFile && fs.existsSync(runtimeFile) && fs.statSync(runtimeFile).isFile()) return true;

    // Keep a read-only legacy fallback for installations that still contain an
    // older in-package upload while they migrate to external runtime storage.
    const publicRoot = path.join(process.cwd(), "public");
    const filePath = path.join(publicRoot, String(value || "").replace(/^\/+/, ""));
    const relative = path.relative(publicRoot, filePath);
    if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
    return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
  } catch {
    return false;
  }
}

export function adminUploadBelongsToSlot(slotKey: string, value?: string) {
  if (!isAdminUploadUrl(value)) return true;
  const filename = String(value || "").split("/").pop() || "";
  const prefix = `${slotSafeName(slotKey)}-`;
  return filename.startsWith(prefix);
}

export const marlothiSafariHomeImageSlotKeys = [
  "img_stay_stay_hero",
  "img_stay_stay_exterior",
  "img_stay_stay_bedroom_main",
  "img_stay_stay_bedroom_second",
  "img_stay_stay_second_level_double_single",
  "img_stay_stay_second_level_double_bed",
  "img_stay_stay_living",
  "img_stay_stay_kitchen",
  "img_stay_stay_washing_up_room",
  "img_stay_stay_bathroom",
  "img_stay_stay_patio_braai",
  "img_stay_stay_on_the_deck",
  "img_stay_stay_pool",
  "img_stay_stay_wildlife_close_home_1",
  "img_stay_stay_wildlife_close_home_2",
  "img_stay_stay_wildlife_close_home_3",
  "img_stay_stay_home_additional_1",
  "img_stay_stay_home_additional_2",
] as const;

export const marlothiSafariHomeWildlifeSlotDefaults = {
  img_stay_stay_wildlife_close_home_1: {
    key: "img_stay_stay_wildlife_close_home_1",
    label: "Stay Wildlife Close To Home 1",
    description: "Marlotii Safari House wildlife close to the home image one.",
    url: "/images/stay/stay-wildlife-close-home-1.webp",
    fallbackUrl: "/images/stay/stay-wildlife-close-home-1.webp",
    alt: "Wildlife close to Marlotii Safari House in Marloth Park",
  },
  img_stay_stay_wildlife_close_home_2: {
    key: "img_stay_stay_wildlife_close_home_2",
    label: "Stay Wildlife Close To Home 2",
    description: "Marlotii Safari House wildlife near the house image two.",
    url: "/images/stay/stay-wildlife-close-home-2.webp",
    fallbackUrl: "/images/stay/stay-wildlife-close-home-2.webp",
    alt: "Wildlife near Marlotii Safari House in Marloth Park",
  },
  img_stay_stay_wildlife_close_home_3: {
    key: "img_stay_stay_wildlife_close_home_3",
    label: "Stay Wildlife Close To Home 3",
    description: "Third Marlotii Safari House wildlife image close to the house.",
    url: "/images/stay/stay-wildlife-close-home-3.webp",
    fallbackUrl: "/images/stay/stay-wildlife-close-home-3.webp",
    alt: "Third wildlife photograph close to Marlotii Safari House",
  },
  img_stay_stay_home_additional_1: {
    key: "img_stay_stay_home_additional_1",
    label: "Marlotii Additional Home View 1",
    description: "Independent additional Marlotii Safari House image block one.",
    url: "/images/stay/stay-home-additional-1.webp",
    fallbackUrl: "/images/stay/stay-home-additional-1.webp",
    alt: "Additional view of Marlotii Safari House",
  },
  img_stay_stay_home_additional_2: {
    key: "img_stay_stay_home_additional_2",
    label: "Marlotii Additional Home View 2",
    description: "Independent additional Marlotii Safari House image block two.",
    url: "/images/stay/stay-home-additional-2.webp",
    fallbackUrl: "/images/stay/stay-home-additional-2.webp",
    alt: "Second additional view of Marlotii Safari House",
  },
} as const;

export const homepageBundleImageSlotDefaults = {
  img_homepage_route_atlas_bundle_image: {
    key: "img_homepage_route_atlas_bundle_image",
    label: "Homepage Route Atlas Guide Image",
    description: "Homepage homepage placement image for the Route Atlas Guide card. Independent admin slot and public file destination.",
    url: "/images/homepage/route-atlas-bundle-card.webp",
    fallbackUrl: "/images/homepage/route-atlas-bundle-card.webp",
    alt: "Route Atlas Guide planning pack image for Marloth Park visitors",
  },
  img_homepage_first_time_visitors_bundle_image: {
    key: "img_homepage_first_time_visitors_bundle_image",
    label: "Homepage First-Time Visitors Guide Image",
    description: "Homepage homepage placement image for the First-Time Visitors Guide card. Independent admin slot and public file destination.",
    url: "/images/homepage/first-time-visitors-bundle-card.webp",
    fallbackUrl: "/images/homepage/first-time-visitors-bundle-card.webp",
    alt: "First-Time Visitors Guide planning pack image for Marloth Park and Kruger guests",
  },
} as const;

export const independentImageSlotDefaults = {
  ...marlothiSafariHomeWildlifeSlotDefaults,
  ...homepageBundleImageSlotDefaults,
} as const;

const legacySharedHomepageBundleUrl = "/images/heroes/first-time-marloth-kruger-visitor-bundle-hero.webp";

function cleanComparableUrl(value?: string) {
  return String(value || "").trim().split("?")[0];
}

function slotSpecificPackagedDefault(slotKey: string, baseSlots?: ImageSlot[]) {
  const wildlifeDefault = marlothiSafariHomeWildlifeSlotDefaults[slotKey as keyof typeof marlothiSafariHomeWildlifeSlotDefaults];
  const baseSlot = (baseSlots || []).find((slot) => slot.key === slotKey);
  return {
    ...(baseSlot || {}),
    ...(wildlifeDefault || {}),
  } as Partial<ImageSlot> & { key?: string };
}

function isSlotSpecificUrl(slotKey: string, url?: string, baseSlots?: ImageSlot[]) {
  const clean = cleanComparableUrl(url);
  if (!clean) return false;
  if (isAdminUploadUrl(clean)) return adminUploadBelongsToSlot(slotKey, clean);
  const defaults = slotSpecificPackagedDefault(slotKey, baseSlots);
  return clean === cleanComparableUrl(defaults.url) || clean === cleanComparableUrl(defaults.fallbackUrl);
}

export function repairMarlotiiSafariHomeImageSlots(slots: ImageSlot[], baseSlots: ImageSlot[] = []) {
  const slotMap = new Map((slots || []).map((slot) => [slot.key, { ...slot }]));

  for (const key of marlothiSafariHomeImageSlotKeys) {
    const defaults = slotSpecificPackagedDefault(key, baseSlots);
    if (!slotMap.has(key) && defaults.key) slotMap.set(key, defaults as ImageSlot);
  }

  for (const [key, defaults] of Object.entries(marlothiSafariHomeWildlifeSlotDefaults)) {
    const existing = slotMap.get(key);
    const currentUrl = cleanComparableUrl(existing?.url);
    const hasBadSharedLegacyPath = currentUrl === "/images/stay/stay-exterior.webp" || currentUrl === "/images/stay/stay-pool.webp";
    const hasWrongUpload = isAdminUploadUrl(currentUrl) && !adminUploadBelongsToSlot(key, currentUrl);
    slotMap.set(key, {
      ...defaults,
      ...(existing || {}),
      key,
      label: existing?.label || defaults.label,
      description: existing?.description || defaults.description,
      alt: existing?.alt || defaults.alt,
      url: !currentUrl || hasBadSharedLegacyPath || hasWrongUpload ? defaults.url : existing!.url,
      fallbackUrl: defaults.fallbackUrl,
      updatedAt: existing?.updatedAt || new Date().toISOString(),
    } as ImageSlot);
  }

  const seen = new Map<string, string>();
  for (const key of marlothiSafariHomeImageSlotKeys) {
    const slot = slotMap.get(key);
    if (!slot) continue;
    const clean = cleanComparableUrl(slot.url);
    const defaults = slotSpecificPackagedDefault(key, baseSlots);
    const defaultUrl = defaults.url || slot.url || "/images/slots/supplier-default.webp";
    const duplicateOwner = clean ? seen.get(clean) : "";
    const invalidUpload = isAdminUploadUrl(clean) && !adminUploadBelongsToSlot(key, clean);
    if (!clean || invalidUpload || (duplicateOwner && !isSlotSpecificUrl(key, clean, baseSlots))) {
      slotMap.set(key, { ...slot, url: defaultUrl, fallbackUrl: defaults.fallbackUrl || defaultUrl, updatedAt: new Date().toISOString() });
      seen.set(cleanComparableUrl(defaultUrl), key);
      continue;
    }
    if (duplicateOwner && isSlotSpecificUrl(key, clean, baseSlots)) {
      const ownerSlot = slotMap.get(duplicateOwner);
      const ownerDefaults = slotSpecificPackagedDefault(duplicateOwner, baseSlots);
      if (ownerSlot && clean !== cleanComparableUrl(ownerDefaults.url)) {
        slotMap.set(duplicateOwner, { ...ownerSlot, url: ownerDefaults.url || ownerSlot.url, fallbackUrl: ownerDefaults.fallbackUrl || ownerDefaults.url || ownerSlot.fallbackUrl, updatedAt: new Date().toISOString() });
      } else {
        slotMap.set(key, { ...slot, url: defaultUrl, fallbackUrl: defaults.fallbackUrl || defaultUrl, updatedAt: new Date().toISOString() });
      }
      continue;
    }
    if (clean) seen.set(clean, key);
  }

  return (slots || []).map((slot) => slotMap.get(slot.key) || slot).concat(
    marlothiSafariHomeImageSlotKeys
      .filter((key) => !(slots || []).some((slot) => slot.key === key) && slotMap.has(key))
      .map((key) => slotMap.get(key) as ImageSlot)
  );
}


function slotDefaultFromBase(slotKey: string, baseSlots: ImageSlot[] = []) {
  const packaged = baseSlots.find((slot) => slot.key === slotKey);
  const forced = independentImageSlotDefaults[slotKey as keyof typeof independentImageSlotDefaults];
  return { ...(packaged || {}), ...(forced || {}) } as Partial<ImageSlot> & { key?: string };
}

function slotDefaultUrl(slotKey: string, baseSlots: ImageSlot[] = [], fallback = "/images/slots/supplier-default.webp") {
  const defaults = slotDefaultFromBase(slotKey, baseSlots);
  return cleanComparableUrl(defaults.url) || cleanComparableUrl(defaults.fallbackUrl) || fallback;
}

function slotDefaultFallbackUrl(slotKey: string, baseSlots: ImageSlot[] = [], fallback?: string) {
  const defaults = slotDefaultFromBase(slotKey, baseSlots);
  return cleanComparableUrl(defaults.fallbackUrl) || cleanComparableUrl(defaults.url) || fallback || "/images/slots/supplier-default.webp";
}

function repairKnownIndependentImageSlots(slots: ImageSlot[], baseSlots: ImageSlot[] = []) {
  const slotMap = new Map((slots || []).map((slot) => [slot.key, { ...slot }]));

  for (const [key, defaults] of Object.entries(independentImageSlotDefaults)) {
    const existing = slotMap.get(key);
    const currentUrl = cleanComparableUrl(existing?.url);
    const wrongUpload = isAdminUploadUrl(currentUrl) && !adminUploadBelongsToSlot(key, currentUrl);
    const missingUpload = isAdminUploadUrl(currentUrl) && adminUploadBelongsToSlot(key, currentUrl) && !adminUploadFileExists(currentUrl);
    const legacyHomepageShare = (key === "img_homepage_route_atlas_bundle_image" || key === "img_homepage_first_time_visitors_bundle_image") && currentUrl === legacySharedHomepageBundleUrl;
    slotMap.set(key, {
      ...defaults,
      ...(existing || {}),
      key,
      label: existing?.label || defaults.label,
      description: defaults.description || existing?.description,
      alt: existing?.alt || defaults.alt,
      url: !currentUrl || wrongUpload || missingUpload || legacyHomepageShare ? defaults.url : existing!.url,
      fallbackUrl: defaults.fallbackUrl,
      updatedAt: existing?.updatedAt || new Date().toISOString(),
    } as ImageSlot);
  }

  const repaired = (slots || []).map((slot) => slotMap.get(slot.key) || slot).concat(
    Object.keys(independentImageSlotDefaults)
      .filter((key) => !(slots || []).some((slot) => slot.key === key) && slotMap.has(key))
      .map((key) => slotMap.get(key) as ImageSlot),
  );

  return repaired.map((slot) => {
    if (!slot?.key) return slot;
    const clean = cleanComparableUrl(slot.url);
    const wrongUpload = isAdminUploadUrl(clean) && !adminUploadBelongsToSlot(slot.key, clean);
    const missingUpload = isAdminUploadUrl(clean) && adminUploadBelongsToSlot(slot.key, clean) && !adminUploadFileExists(clean);
    if (!wrongUpload && !missingUpload) return slot;
    const defaultUrl = slotDefaultUrl(slot.key, baseSlots, slot.fallbackUrl || "/images/slots/supplier-default.webp");
    return { ...slot, url: defaultUrl, fallbackUrl: slotDefaultFallbackUrl(slot.key, baseSlots, defaultUrl), updatedAt: new Date().toISOString() };
  });
}

function repairDuplicateSlotPaths(slots: ImageSlot[], baseSlots: ImageSlot[] = []) {
  const list = (slots || []).map((slot) => ({ ...slot }));
  const keySet = new Set<string>();
  const uniqueKeyList: ImageSlot[] = [];
  for (const slot of list) {
    if (!slot?.key || keySet.has(slot.key)) continue;
    keySet.add(slot.key);
    uniqueKeyList.push(slot);
  }

  const seenUrl = new Map<string, string>();
  const repaired = uniqueKeyList.map((slot) => {
    const clean = cleanComparableUrl(slot.url);
    if (!clean) {
      const defaultUrl = slotDefaultUrl(slot.key, baseSlots);
      return { ...slot, url: defaultUrl, fallbackUrl: slotDefaultFallbackUrl(slot.key, baseSlots, defaultUrl), updatedAt: new Date().toISOString() };
    }

    const owner = seenUrl.get(clean);
    if (!owner) {
      seenUrl.set(clean, slot.key);
      return slot;
    }

    const ownerDefault = slotDefaultUrl(owner, baseSlots);
    const slotDefault = slotDefaultUrl(slot.key, baseSlots);
    if (clean !== slotDefault && slotDefault && slotDefault !== clean) {
      seenUrl.set(slotDefault, slot.key);
      return { ...slot, url: slotDefault, fallbackUrl: slotDefaultFallbackUrl(slot.key, baseSlots, slotDefault), updatedAt: new Date().toISOString() };
    }

    if (clean !== ownerDefault) {
      seenUrl.set(slotDefault, slot.key);
      return { ...slot, url: slotDefault, fallbackUrl: slotDefaultFallbackUrl(slot.key, baseSlots, slotDefault), updatedAt: new Date().toISOString() };
    }

    return slot;
  });

  const seenFallback = new Map<string, string>();
  return repaired.map((slot) => {
    const fallback = cleanComparableUrl(slot.fallbackUrl);
    if (!fallback) return { ...slot, fallbackUrl: slotDefaultFallbackUrl(slot.key, baseSlots, slot.url) };
    const owner = seenFallback.get(fallback);
    if (!owner) {
      seenFallback.set(fallback, slot.key);
      return slot;
    }
    const slotFallback = slotDefaultFallbackUrl(slot.key, baseSlots, slot.url);
    if (slotFallback !== fallback) {
      seenFallback.set(slotFallback, slot.key);
      return { ...slot, fallbackUrl: slotFallback, updatedAt: new Date().toISOString() };
    }
    return { ...slot, fallbackUrl: slot.url || slotFallback, updatedAt: new Date().toISOString() };
  });
}

export function repairIndependentImageSlots(slots: ImageSlot[], baseSlots: ImageSlot[] = []) {
  return repairDuplicateSlotPaths(repairKnownIndependentImageSlots(repairMarlotiiSafariHomeImageSlots(slots, baseSlots), baseSlots), baseSlots);
}

export function trustedSlotImageUrl(slotKey: string, preferredUrl?: string, fallbackUrl?: string, hardFallback = "/images/slots/supplier-default.webp") {
  const preferred = String(preferredUrl || "");
  const fallback = String(fallbackUrl || "");
  if (preferred && adminUploadBelongsToSlot(slotKey, preferred) && adminUploadFileExists(preferred)) return preferred;
  if (fallback && adminUploadBelongsToSlot(slotKey, fallback) && adminUploadFileExists(fallback)) return fallback;
  if (fallback && !isAdminUploadUrl(fallback)) return fallback;
  return hardFallback;
}

function isNewer(left?: string, right?: string) {
  const a = Date.parse(String(left || ""));
  const b = Date.parse(String(right || ""));
  if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
  return a > b;
}

export function hardenImageSlotForPublic(slot: ImageSlot): ImageSlot {
  if (!slot?.key) return slot;
  const url = String(slot.url || "");
  if (!isAdminUploadUrl(url)) return slot;
  if (adminUploadBelongsToSlot(slot.key, url) && adminUploadFileExists(url)) return slot;
  return {
    ...slot,
    url: trustedSlotImageUrl(slot.key, slot.fallbackUrl, undefined, slot.fallbackUrl || "/images/slots/supplier-default.webp"),
  };
}

export function hardenImageSlotsForPublic(slots: ImageSlot[]) {
  return (slots || []).map(hardenImageSlotForPublic);
}

export function protectImageSlotsOnAdminSave(incomingSlots: ImageSlot[], currentSlots: ImageSlot[]) {
  const currentByKey = new Map((currentSlots || []).map((slot) => [slot.key, slot]));

  const protectedSlots = (incomingSlots || []).map((incoming) => {
    const current = currentByKey.get(incoming.key);
    const incomingHasWrongAdminUpload = isAdminUploadUrl(incoming.url) && !adminUploadBelongsToSlot(incoming.key, incoming.url);
    const incomingHasMissingAdminUpload = isAdminUploadUrl(incoming.url) && adminUploadBelongsToSlot(incoming.key, incoming.url) && !adminUploadFileExists(incoming.url);
    const currentHasTrustedUpload = Boolean(current && isAdminUploadUrl(current.url) && adminUploadBelongsToSlot(current.key, current.url) && adminUploadFileExists(current.url));
    const currentUploadIsNewer = currentHasTrustedUpload && isNewer(current?.updatedAt, incoming.updatedAt);

    if (current && (currentUploadIsNewer || incomingHasWrongAdminUpload || incomingHasMissingAdminUpload)) {
      return {
        ...incoming,
        url: currentHasTrustedUpload ? current.url : trustedSlotImageUrl(incoming.key, incoming.fallbackUrl, current.fallbackUrl),
        fallbackUrl: current.fallbackUrl || incoming.fallbackUrl,
        updatedAt: currentUploadIsNewer ? current.updatedAt : incoming.updatedAt,
      };
    }

    if (incomingHasWrongAdminUpload || incomingHasMissingAdminUpload) {
      return {
        ...incoming,
        url: trustedSlotImageUrl(incoming.key, incoming.fallbackUrl, undefined, incoming.fallbackUrl || "/images/slots/supplier-default.webp"),
      };
    }

    return incoming;
  });

  return repairIndependentImageSlots(protectedSlots, currentSlots || incomingSlots || []);
}

export function imageSlotIntegrityProblems(slots: ImageSlot[]) {
  const problems: { key: string; label: string; url: string; issue: string }[] = [];
  const keyCounts = new Map<string, number>();
  const urlToKeys = new Map<string, string[]>();
  const fallbackUrlToKeys = new Map<string, string[]>();

  for (const slot of slots || []) {
    if (!slot?.key) continue;
    keyCounts.set(slot.key, (keyCounts.get(slot.key) || 0) + 1);

    const cleanUrl = cleanComparableUrl(slot.url);
    const cleanFallback = cleanComparableUrl(slot.fallbackUrl);
    if (cleanUrl) {
      const keys = urlToKeys.get(cleanUrl) || [];
      keys.push(slot.key);
      urlToKeys.set(cleanUrl, keys);
    }
    if (cleanFallback) {
      const keys = fallbackUrlToKeys.get(cleanFallback) || [];
      keys.push(slot.key);
      fallbackUrlToKeys.set(cleanFallback, keys);
    }

    if (isAdminUploadUrl(cleanUrl)) {
      if (!adminUploadBelongsToSlot(slot.key, cleanUrl)) {
        problems.push({ key: slot.key, label: slot.label || slot.key, url: cleanUrl, issue: "uploaded file belongs to a different image slot" });
      }
      if (!adminUploadFileExists(cleanUrl)) {
        problems.push({ key: slot.key, label: slot.label || slot.key, url: cleanUrl, issue: "uploaded file is missing from public/uploads/admin" });
      }
    }
  }

  for (const [key, count] of keyCounts.entries()) {
    if (count > 1) problems.push({ key, label: key, url: "", issue: `duplicate image slot key appears ${count} times` });
  }

  for (const [url, keys] of urlToKeys.entries()) {
    if (keys.length > 1) {
      for (const key of keys) {
        const slot = slots.find((item) => item.key === key);
        problems.push({ key, label: slot?.label || key, url, issue: `same image URL/write target is assigned to ${keys.length} image slots: ${keys.join(", ")}` });
      }
    }
  }

  for (const [url, keys] of fallbackUrlToKeys.entries()) {
    if (keys.length > 1) {
      for (const key of keys) {
        const slot = slots.find((item) => item.key === key);
        problems.push({ key, label: slot?.label || key, url, issue: `same fallback URL is assigned to ${keys.length} image slots: ${keys.join(", ")}` });
      }
    }
  }

  return problems;
}
