/**
 * Landing Page Publish Snapshot
 *
 * A published landing page is a STATIC version. The moment a deployment is
 * enqueued we freeze everything that goes into the live page — the generated
 * index.html (with the user's theme customisation already baked in), the images,
 * and the publish-time page settings (SEO, tracking, embeds, lead capture) — into
 * a per-deployment snapshot directory.
 *
 * The deploy worker bundles from that snapshot, never from the live artifact. So:
 *   - edits made in the platform after hitting Publish never leak into that deploy
 *     (the worker runs asynchronously and retries with backoff — without this it
 *     would upload whatever happened to be on disk minutes later);
 *   - the live page never changes on its own afterwards. Updating it requires
 *     publishing again, which captures a fresh snapshot.
 *
 * Pure filesystem + plain objects; no models, no network.
 */

import fs from 'fs';
import path from 'path';
import crypto from 'crypto';

/** Root for frozen publish artifacts, kept separate from the live generator output. */
export function getSnapshotRoot(): string {
  return path.join(process.cwd(), 'uploads', 'landing-page-snapshots');
}

/** Reject anything that could escape the snapshot root. */
function safeId(id: string): string | null {
  const value = String(id || '');
  if (!value || value.includes('/') || value.includes('\\') || value.includes('..')) return null;
  return value;
}

export function getSnapshotDir(deploymentId: string): string | null {
  const id = safeId(deploymentId);
  return id ? path.join(getSnapshotRoot(), id) : null;
}

export interface PublishSnapshot {
  /** Absolute path of the frozen artifact directory */
  dir: string;
  /** Id the generator keyed its image URLs on (the live artifact directory's name) */
  artifactId: string;
  capturedAt: Date;
  /** Fingerprint of exactly what was frozen — used to detect later platform edits */
  contentHash: string;
  fileCount: number;
  /** Publish-time copy of the page settings the bundler reads */
  pageSettings: Record<string, any>;
  /** Publish-time copy of the manual colour / CTA customisation (for display + audit) */
  themeCustomization: Record<string, any> | null;
}

/**
 * The page fields that actually influence the published output (see landingPageBundler:
 * buildHeadInjection / buildBodyInjection / rewireLeadForms / buildSitemap).
 *
 * Snapshotting this explicit subset — rather than the whole page — keeps the record
 * small and makes it obvious which settings are frozen at publish time.
 */
const PUBLISH_PAGE_FIELDS = [
  'id',
  'name',
  'metaTitle',
  'metaDescription',
  'seo',
  'tracking',
  'embeds',
  'leadCapture',
  'themeCustomization',
] as const;

/** Build the frozen copy of the publish-relevant page settings. */
export function capturePageSettings(page: any): Record<string, any> {
  const settings: Record<string, any> = {};
  for (const field of PUBLISH_PAGE_FIELDS) {
    const value = page?.[field];
    if (value !== undefined) settings[field] = value;
  }
  // The sitemap uses the previously deployed URL; keep only that, not the whole
  // live deployment status (which keeps changing as this very deploy progresses).
  if (page?.deployment?.deployUrl) {
    settings.deployment = { deployUrl: page.deployment.deployUrl };
  }
  // Deep-clone through JSON so later mutations of the live page document can never
  // reach back into the frozen copy.
  try {
    return JSON.parse(JSON.stringify(settings));
  } catch {
    return settings;
  }
}

/** All files under a directory, as sorted "relpath:sha1" lines. */
function fingerprintDir(dir: string): string[] {
  const entries: string[] = [];
  const walk = (current: string) => {
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      const abs = path.join(current, entry.name);
      if (entry.isDirectory()) {
        walk(abs);
      } else {
        const rel = path.relative(dir, abs).split(path.sep).join('/');
        const hash = crypto.createHash('sha1').update(fs.readFileSync(abs)).digest('hex');
        entries.push(`${rel}:${hash}`);
      }
    }
  };
  if (fs.existsSync(dir)) walk(dir);
  entries.sort();
  return entries;
}

/** JSON with object keys sorted at every level, so key order alone can't change the hash. */
function stableStringify(value: any): string {
  if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  const keys = Object.keys(value).sort();
  return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
}

/**
 * Fingerprint an artifact directory + page settings.
 *
 * Comparing the live artifact's hash against a live deployment's stored hash is how
 * "you have unpublished changes" is decided — same inputs, same function, both sides.
 */
export function computeContentHash(dir: string, pageSettings: Record<string, any>): string {
  const files = fingerprintDir(dir);
  // `deployment.deployUrl` only feeds the sitemap and is written BY publishing, so
  // including it would make every page look "changed" the moment it first goes live.
  const { deployment: _deployUrlOnly, ...hashable } = pageSettings || {};
  let settingsJson = '';
  try {
    settingsJson = stableStringify(hashable);
  } catch {
    settingsJson = '';
  }
  return crypto
    .createHash('sha256')
    .update(files.join('|'))
    .update(' ')
    .update(settingsJson)
    .digest('hex')
    .slice(0, 32);
}

/** Hash of the CURRENT (unpublished) state of a page, for drift comparison. */
export function computeCurrentContentHash(artifactDir: string, page: any): string {
  return computeContentHash(artifactDir, capturePageSettings(page));
}

/**
 * Freeze the generated artifact + page settings for a deployment.
 *
 * Copies the whole artifact directory so the published files are immune to any later
 * regeneration, theme change, or deletion of the live page.
 */
export function createDeploymentSnapshot(
  deploymentId: string,
  artifactDir: string,
  page: any,
): PublishSnapshot {
  const dir = getSnapshotDir(deploymentId);
  if (!dir) throw new Error('Invalid deployment id for snapshot');
  if (!fs.existsSync(path.join(artifactDir, 'index.html'))) {
    throw new Error('No generated index.html to snapshot');
  }

  fs.rmSync(dir, { recursive: true, force: true });
  fs.mkdirSync(dir, { recursive: true });
  fs.cpSync(artifactDir, dir, { recursive: true });

  const pageSettings = capturePageSettings(page);
  const files = fingerprintDir(dir);

  return {
    dir,
    artifactId: path.basename(artifactDir),
    capturedAt: new Date(),
    contentHash: computeContentHash(dir, pageSettings),
    fileCount: files.length,
    pageSettings,
    themeCustomization: page?.themeCustomization || null,
  };
}

/** Remove a deployment's frozen artifact (cancelled / permanently failed / superseded). */
export function deleteDeploymentSnapshot(deploymentId: string): void {
  const dir = getSnapshotDir(deploymentId);
  if (!dir) return;
  try {
    fs.rmSync(dir, { recursive: true, force: true });
  } catch (err: any) {
    console.warn(`[LandingPageSnapshot] Could not remove snapshot ${deploymentId}:`, err?.message);
  }
}

/** True when a snapshot directory still holds a usable artifact. */
export function snapshotExists(dir: string | undefined | null): boolean {
  return Boolean(dir && fs.existsSync(path.join(dir, 'index.html')));
}
