import { existsSync } from "fs";
import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
import path from "path";
import { storageDirectory } from "@/lib/runtime/config";
import { listMediaFiles } from "@/lib/admin/media";
import { contentFileNames, defaultContent } from "./defaults";
import type { ContentBackup, ContentDocuments, ContentKey, ContentSummary } from "./types";

export const contentDirectory = path.join(storageDirectory, "content");
export const contentBackupDirectory = path.join(contentDirectory, "backups");
export const contentVersion = "0.4.1";

const contentKeys = Object.keys(contentFileNames) as ContentKey[];

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function sanitizeString(value: string) {
  return value
    .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
    .replace(/javascript:/gi, "")
    .replace(/[<>]/g, "")
    .trim();
}

function sanitizeValue(value: unknown): unknown {
  if (typeof value === "string") return sanitizeString(value);
  if (Array.isArray(value)) return value.map(sanitizeValue);
  if (!isRecord(value)) return value;

  return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeValue(entry)]));
}

function mergeContent<T>(defaults: T, incoming: unknown): T {
  if (Array.isArray(defaults)) return Array.isArray(incoming) ? sanitizeValue(incoming) as T : defaults;
  if (!isRecord(defaults)) return sanitizeValue(incoming ?? defaults) as T;
  if (!isRecord(incoming)) return defaults;

  const merged: Record<string, unknown> = { ...defaults };
  for (const [key, value] of Object.entries(defaults)) {
    merged[key] = mergeContent(value, incoming[key]);
  }
  return sanitizeValue(merged) as T;
}

function filePathFor(key: ContentKey) {
  return path.join(contentDirectory, contentFileNames[key]);
}

function backupStamp() {
  const date = new Date();
  const pad = (value: number) => String(value).padStart(2, "0");
  return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}

async function pruneBackups(key: ContentKey) {
  const prefix = `${key}.`;
  const entries = await readdir(contentBackupDirectory).catch(() => []);
  const backups = await Promise.all(entries
    .filter((entry) => entry.startsWith(prefix) && entry.endsWith(".json"))
    .map(async (entry) => {
      const fullPath = path.join(contentBackupDirectory, entry);
      const fileStat = await stat(fullPath);
      return { entry, fullPath, modified: fileStat.mtimeMs };
    }));

  const obsolete = backups.sort((left, right) => right.modified - left.modified).slice(20);
  await Promise.all(obsolete.map((entry) => unlink(entry.fullPath).catch(() => undefined)));
}

async function ensureContentDocument<K extends ContentKey>(key: K) {
  await mkdir(contentDirectory, { recursive: true });
  await mkdir(contentBackupDirectory, { recursive: true });

  const target = filePathFor(key);
  if (!existsSync(target)) {
    await writeFile(target, `${JSON.stringify(defaultContent[key], null, 2)}\n`, "utf8");
  }
}

export async function ensureContentStorage() {
  await mkdir(contentDirectory, { recursive: true });
  await mkdir(contentBackupDirectory, { recursive: true });
  await Promise.all(contentKeys.map((key) => ensureContentDocument(key)));
}

export async function getContent<K extends ContentKey>(key: K): Promise<ContentDocuments[K]> {
  await ensureContentDocument(key);

  try {
    const raw = await readFile(filePathFor(key), "utf8");
    const parsed = JSON.parse(raw) as unknown;
    return mergeContent(defaultContent[key], parsed);
  } catch {
    return defaultContent[key];
  }
}

export async function getAllContent(): Promise<ContentDocuments> {
  const entries = await Promise.all(contentKeys.map(async (key) => [key, await getContent(key)] as const));
  return Object.fromEntries(entries) as ContentDocuments;
}

export async function saveContent<K extends ContentKey>(key: K, content: ContentDocuments[K]) {
  await ensureContentDocument(key);
  const current = filePathFor(key);
  const backup = path.join(contentBackupDirectory, `${key}.${backupStamp()}.json`);

  if (existsSync(current)) await rename(current, backup);

  const next = mergeContent(defaultContent[key], {
    ...content,
    updatedAt: new Date().toISOString(),
  });

  await writeFile(current, `${JSON.stringify(next, null, 2)}\n`, "utf8");
  await pruneBackups(key);
  return next;
}

export async function getContentSummary(): Promise<ContentSummary> {
  await ensureContentStorage();
  const documents = await Promise.all(contentKeys.map(async (key) => {
    const content = await getContent(key);
    return {
      key,
      fileName: contentFileNames[key],
      title: "title" in content ? content.title : content.siteName,
      updatedAt: content.updatedAt,
    };
  }));
  const lastModified = documents
    .map((document) => document.updatedAt)
    .filter(Boolean)
    .sort()
    .at(-1) ?? null;

  return {
    version: contentVersion,
    lastModified,
    pageCount: documents.length,
    documents,
  };
}

export async function listContentBackups(limit = 20): Promise<ContentBackup[]> {
  await ensureContentStorage();
  const entries = await readdir(contentBackupDirectory).catch(() => []);
  const backups = await Promise.all(entries
    .filter((entry) => entry.endsWith(".json"))
    .map(async (entry) => {
      const page = entry.split(".")[0] as ContentKey;
      const fullPath = path.join(contentBackupDirectory, entry);
      const fileStat = await stat(fullPath);
      return {
        page,
        fileName: entry,
        createdAt: fileStat.mtime.toISOString(),
      };
    }));

  return backups
    .filter((backup) => contentKeys.includes(backup.page))
    .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
    .slice(0, limit);
}

export async function getContentSystemStatus() {
  const [summary, media, backups] = await Promise.all([getContentSummary(), listMediaFiles(), listContentBackups()]);
  return {
    ...summary,
    mediaCount: media.length,
    backups,
  };
}
