/**
 * Cookie consent state (client only).
 *
 * The site sets no non-essential cookies by default. Analytics loads only when a GA4 ID is
 * configured AND the visitor opts in. A withdrawal takes effect in the same page view: Consent
 * Mode is updated to denied and the Google Analytics cookies are deleted.
 *
 * Anything stored here is kept on the visitor's own device and is never sent to us.
 */

export const CONSENT_STORAGE_KEY = "axidys-consent-v2";
export const CONSENT_EVENT = "axidys:consent-change";
export const CONSENT_OPEN_EVENT = "axidys:consent-open";

/** Bump when the categories change, so an old record is asked again instead of assumed. */
export const CONSENT_VERSION = 2;

/** A recorded choice lapses after this long and the banner asks again. */
export const CONSENT_MAX_AGE_DAYS = 365;

/** Every optional category the banner and the preference centre can switch on. */
export const optionalCategories = ["analytics"] as const;
export type OptionalCategory = (typeof optionalCategories)[number];

/** Where a recorded choice came from. Shown on the preference centre so the record is visible. */
export type ConsentSource = "banner" | "preferences";

export type ConsentChoices = Record<OptionalCategory, boolean>;

export interface ConsentState extends ConsentChoices {
  version: number;
  source: ConsentSource;
  updatedAt: string;
}

export interface ResolvedConsent {
  /** The recorded choice, or null when none exists or the record has lapsed. */
  stored: ConsentState | null;
  /** What the site acts on right now. */
  effective: ConsentChoices;
  /** True when the browser sent Global Privacy Control and no explicit choice overrides it. */
  honouringSignal: boolean;
  /** True when the banner should ask for a choice. */
  shouldAsk: boolean;
}

export const denyAll: ConsentChoices = { analytics: false };
export const grantAll: ConsentChoices = { analytics: true };

const GA_ID_PATTERN = /^G-[A-Z0-9]{4,16}$/;
const DAY_MS = 24 * 60 * 60 * 1000;

/** Used when localStorage is unavailable, so a choice still holds for the current page view. */
let memoryFallback: string | null = null;

export function getAnalyticsId(): string | null {
  const id = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID?.trim();
  return id && GA_ID_PATTERN.test(id) ? id : null;
}

/** True when at least one optional technology exists to ask about. Otherwise no banner is shown. */
export function hasOptionalTechnologies(): boolean {
  return getAnalyticsId() !== null;
}

/** Reads the Global Privacy Control signal, which we treat as a refusal of optional categories. */
export function hasGlobalPrivacyControl(): boolean {
  return (navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl === true;
}

export function parseConsent(raw: string | null, now = Date.now()): ConsentState | null {
  if (!raw) return null;
  try {
    const value = JSON.parse(raw) as Partial<ConsentState>;
    if (value.version !== CONSENT_VERSION) return null;
    if (typeof value.analytics !== "boolean" || typeof value.updatedAt !== "string") return null;
    const recordedAt = Date.parse(value.updatedAt);
    if (!Number.isFinite(recordedAt) || now - recordedAt > CONSENT_MAX_AGE_DAYS * DAY_MS) return null;
    return {
      version: CONSENT_VERSION,
      analytics: value.analytics,
      source: value.source === "preferences" ? "preferences" : "banner",
      updatedAt: value.updatedAt,
    };
  } catch {
    return null;
  }
}

export function readConsentRaw(): string | null {
  try {
    return window.localStorage.getItem(CONSENT_STORAGE_KEY) ?? memoryFallback;
  } catch {
    return memoryFallback;
  }
}

/** Turns the stored record, the browser signal, and what is configured into one answer. */
export function resolveConsent(raw: string | null, now = Date.now()): ResolvedConsent {
  const stored = parseConsent(raw, now);
  if (stored) {
    return { stored, effective: { analytics: stored.analytics }, honouringSignal: false, shouldAsk: false };
  }
  if (hasGlobalPrivacyControl()) {
    return { stored: null, effective: denyAll, honouringSignal: true, shouldAsk: false };
  }
  return { stored: null, effective: denyAll, honouringSignal: false, shouldAsk: hasOptionalTechnologies() };
}

export function writeConsent(choices: ConsentChoices, source: ConsentSource): void {
  const state: ConsentState = {
    version: CONSENT_VERSION,
    analytics: choices.analytics,
    source,
    updatedAt: new Date().toISOString(),
  };
  const serialized = JSON.stringify(state);
  memoryFallback = serialized;
  try {
    window.localStorage.setItem(CONSENT_STORAGE_KEY, serialized);
  } catch {
    // Storage can be unavailable (private mode, blocked site data). The choice then lasts for this page view.
  }
  applyChoices(choices);
  window.dispatchEvent(new Event(CONSENT_EVENT));
}

/** Forgets the recorded choice so the banner asks again. Optional categories go back to off. */
export function clearConsent(): void {
  memoryFallback = null;
  try {
    window.localStorage.removeItem(CONSENT_STORAGE_KEY);
  } catch {
    // Nothing to remove when storage is unavailable.
  }
  applyChoices(denyAll);
  window.dispatchEvent(new Event(CONSENT_EVENT));
}

/** Asks the banner to open, so the footer link can reach it from any page. */
export function openConsentBanner(): void {
  window.dispatchEvent(new Event(CONSENT_OPEN_EVENT));
}

export function subscribeToConsent(callback: () => void): () => void {
  window.addEventListener(CONSENT_EVENT, callback);
  window.addEventListener("storage", callback);
  return () => {
    window.removeEventListener(CONSENT_EVENT, callback);
    window.removeEventListener("storage", callback);
  };
}

export function subscribeToConsentOpen(callback: () => void): () => void {
  window.addEventListener(CONSENT_OPEN_EVENT, callback);
  return () => window.removeEventListener(CONSENT_OPEN_EVENT, callback);
}

function applyChoices(choices: ConsentChoices): void {
  updateConsentMode(choices.analytics);
  if (!choices.analytics) clearAnalyticsCookies();
}

/** Tells Google Consent Mode about a change, so a withdrawal lands without a page reload. */
function updateConsentMode(analytics: boolean): void {
  const gtag = (window as Window & { gtag?: (...args: unknown[]) => void }).gtag;
  gtag?.("consent", "update", { analytics_storage: analytics ? "granted" : "denied" });
}

/**
 * Deletes the Google Analytics cookies. They are set on the registrable domain, which the browser
 * will not tell us, so every parent domain of the current host is tried.
 */
export function clearAnalyticsCookies(): void {
  const names = document.cookie
    .split(";")
    .map((entry) => entry.split("=")[0]?.trim() ?? "")
    .filter((name) => name.startsWith("_ga"));
  if (names.length === 0) return;

  const labels = window.location.hostname.split(".");
  const domains = labels.map((_, index) => `.${labels.slice(index).join(".")}`).filter((domain) => domain.includes(".", 1));

  for (const name of names) {
    document.cookie = `${name}=; path=/; max-age=0; SameSite=Lax`;
    for (const domain of domains) {
      document.cookie = `${name}=; path=/; domain=${domain}; max-age=0; SameSite=Lax`;
    }
  }
}
