#!/usr/bin/env node

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import {
  DATA_ROOT,
  createAsset,
  createImportRun,
  listAssets,
  listCharacters,
  updateImportRun,
  upsertCharacter,
} from "../src/db.js";

const DEFAULT_ROOT_URL = "https://drive.google.com/drive/folders/1Pw2W9gLL5-9u-yS8sXBPJLbFoXqRIdha";
const DRIVE_FOLDER_MIME = "application/vnd.google-apps.folder";
const DRIVE_DOC_MIME = "application/vnd.google-apps.document";
const USER_AGENT = "Mozilla/5.0";
const MAX_IMAGES_PER_CHARACTER = Math.max(
  0,
  Number.parseInt(process.env.SSB_DRIVE_IMPORT_MAX_IMAGES_PER_CHARACTER || "0", 10) || 0
);
const SKIP_RUN_LOG = process.env.SSB_DRIVE_IMPORT_SKIP_RUN_LOG === "1";
const STOP_WORDS = new Set([
  "the",
  "and",
  "of",
  "his",
  "gang",
  "references",
  "reference",
  "shots",
  "dual",
  "manifestion",
  "manifestation",
]);
const PHOTO_FOLDER_PATTERN = /^photos?$/i;
const SLOT_LABELS = {
  portrait: "Portrait",
  select_art: "Select Art",
  reference_sheet: "Reference Sheet",
  stock_icon: "Stock Icon",
  gallery_support: "Gallery Support",
};
const FOLDER_OVERRIDES = {
  "1iMiqlAuwO6Jxa-5JuX6tsU6qkKt1lpfH": {
    slug: "zaela-zhezzai",
    display_name: "Zaela Zhezzaia",
  },
  "1cNeCc50gs7752xNQO6cRiqJvXZPwZC8q": {
    slug: "a-wild-iraqi-reference-shots",
    display_name: "A Wild Iraqi",
  },
  "1cR-wQrg7S09e36-jR-sB56MIeztxOCJ1": {
    slug: "deldemonover9000",
    display_name: "DelDemon",
  },
  "1cV13xIuZNimRrTwb_EVNFepRaT7jXmcR": {
    slug: "dj-nathaniel",
    display_name: "DJ Nathaniel",
  },
  "1879KTTfFm71RZHyoCBnMZ8QeywqJhy-L": {
    slug: "hawk-sinferno-references",
    display_name: "Hawk Sinferno",
  },
  "13STy7nR0qsojlwIQPloKsl6ZsQL0SvIk": {
    slug: "kini-and-his-gang",
    display_name: "KiNi",
  },
};

function decodeDriveString(value) {
  return String(value ?? "")
    .replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
    .replace(/\\\//g, "/")
    .replace(/\\"/g, "\"")
    .replace(/\\\\/g, "\\")
    .trim();
}

function stripBom(value) {
  return String(value || "").replace(/^\uFEFF/, "");
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function asciiFold(value) {
  return decodeDriveString(value)
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "");
}

function looseKey(value) {
  return asciiFold(value)
    .toLowerCase()
    .replace(/&/g, " and ")
    .replace(/\([^)]*\)/g, " ")
    .replace(/[^a-z0-9]+/g, " ")
    .split(/\s+/)
    .filter(Boolean)
    .filter((token) => !STOP_WORDS.has(token))
    .join("-");
}

function slugify(value) {
  return asciiFold(value)
    .toLowerCase()
    .replace(/&/g, " and ")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
}

function cleanDisplayName(value) {
  return decodeDriveString(value)
    .replace(/^!+|!+$/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

function mergeTags(...values) {
  const seen = new Set();
  const result = [];
  for (const raw of values) {
    for (const part of String(raw || "").split(",")) {
      const tag = part.trim();
      if (!tag) continue;
      const key = tag.toLowerCase();
      if (seen.has(key)) continue;
      seen.add(key);
      result.push(tag);
    }
  }
  return result.join(", ");
}

function parseOnlyKeys(value) {
  return new Set(
    String(value || "")
      .split(",")
      .map((part) => looseKey(part))
      .filter(Boolean)
  );
}

function pickProfileSummary(profileText) {
  const lines = stripBom(profileText)
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .filter((line) => !/^(platforms|lore|prompts.*|additional info.*)$/i.test(line))
    .filter((line) => !/^(kick|youtube|twitter|twitter\/x|twitch|instagram|discord)\s*:/i.test(line))
    .filter((line) => !/^https?:\/\//i.test(line));

  return lines.join(" ").slice(0, 420);
}

function sanitizeFilename(filename) {
  const ext = path.extname(filename || "");
  const stem = path.basename(filename || "asset", ext);
  return `${stem}`
    .replace(/[^a-zA-Z0-9._-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 120) || "asset";
}

function mimeToExt(mimeType) {
  const value = String(mimeType || "").toLowerCase();
  if (value.includes("png")) return ".png";
  if (value.includes("jpeg") || value.includes("jpg")) return ".jpg";
  if (value.includes("webp")) return ".webp";
  if (value.includes("gif")) return ".gif";
  return "";
}

function isImageItem(item) {
  return String(item.mime_type || "").toLowerCase().startsWith("image/");
}

function choosePortrait(images, characterName) {
  const charKey = looseKey(characterName).replace(/-/g, "");
  const ranked = [...images].sort((a, b) => portraitScore(b, charKey) - portraitScore(a, charKey) || a.name.localeCompare(b.name));
  return ranked[0] || null;
}

function portraitScore(image, charKey) {
  const stem = asciiFold(path.basename(image.name, path.extname(image.name))).toLowerCase();
  let score = 0;
  if (/(avatar|icon|pfp|portrait|headshot|profile|main|face|bust)/i.test(stem)) score += 80;
  if (charKey && stem.replace(/[^a-z0-9]/g, "").includes(charKey)) score += 30;
  if (/(reference|sheet|expression|weapon|body|variant|young|teen|meme|screenshot)/i.test(stem)) score -= 30;
  return score;
}

function classifySlot(image, portraitId, state) {
  if (image.id === portraitId) {
    return "portrait";
  }

  const stem = asciiFold(path.basename(image.name, path.extname(image.name))).toLowerCase();

  if (!state.stock_icon && /(avatar|icon|pfp|headshot)/i.test(stem)) {
    state.stock_icon = true;
    return "stock_icon";
  }

  if (!state.select_art && /(fullbody|full-body|standing|hero|render|finale|real|character design|design)/i.test(stem)) {
    state.select_art = true;
    return "select_art";
  }

  if (!state.reference_sheet && /(reference|sheet|expression|weapon|body|variant|young|teen|outfit|model|concept|turnaround)/i.test(stem)) {
    state.reference_sheet = true;
    return "reference_sheet";
  }

  return "gallery_support";
}

function limitImages(images) {
  if (!MAX_IMAGES_PER_CHARACTER || images.length <= MAX_IMAGES_PER_CHARACTER) {
    return images;
  }

  const priority = {
    portrait: 0,
    select_art: 1,
    reference_sheet: 2,
    stock_icon: 3,
    gallery_support: 4,
  };

  return [...images]
    .sort((a, b) => (priority[a.slot] ?? 99) - (priority[b.slot] ?? 99) || a.name.localeCompare(b.name))
    .slice(0, MAX_IMAGES_PER_CHARACTER);
}

async function fetchWithRetry(url, options = {}, attempts = 4) {
  let lastError = null;
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          "user-agent": USER_AGENT,
          ...(options.headers || {}),
        },
      });

      if (response.status === 429 || response.status >= 500) {
        const retryMs = attempt * 750;
        await new Promise((resolve) => setTimeout(resolve, retryMs));
        lastError = new Error(`Retryable Drive response ${response.status} for ${url}`);
        continue;
      }

      if (!response.ok) {
        throw new Error(`Drive request failed (${response.status}) for ${url}`);
      }

      return response;
    } catch (error) {
      lastError = error;
      if (attempt === attempts) {
        throw error;
      }
      await new Promise((resolve) => setTimeout(resolve, attempt * 750));
    }
  }

  throw lastError;
}

async function fetchText(url) {
  const response = await fetchWithRetry(url);
  return response.text();
}

function extractChildren(html, parentId) {
  const children = [];
  const seen = new Set();
  const pattern = new RegExp(
    String.raw`\\x22([A-Za-z0-9_-]{20,})\\x22,\\x5b\\x22${escapeRegExp(parentId)}\\x22\\x5d,\\x22((?:[^\\]|\\.)*?)\\x22,\\x22((?:[^\\]|\\.)*?)\\x22`,
    "g"
  );

  let match;
  while ((match = pattern.exec(html))) {
    const [, id, rawName, rawMime] = match;
    if (seen.has(id)) continue;
    seen.add(id);
    children.push({
      id,
      name: decodeDriveString(rawName),
      mime_type: decodeDriveString(rawMime),
      url: `https://drive.google.com/drive/folders/${id}`,
    });
  }

  return children;
}

async function fetchFolderChildren(folderId) {
  const html = await fetchText(`https://drive.google.com/drive/folders/${folderId}`);
  return extractChildren(html, folderId);
}

async function fetchDocText(docId) {
  const response = await fetchWithRetry(`https://docs.google.com/document/d/${docId}/export?format=txt`);
  return stripBom(await response.text()).trim();
}

async function downloadDriveFile(fileId, destinationPath) {
  const response = await fetchWithRetry(`https://drive.google.com/uc?export=download&id=${fileId}`);
  const arrayBuffer = await response.arrayBuffer();
  fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
  fs.writeFileSync(destinationPath, Buffer.from(arrayBuffer));
  return {
    mime_type: response.headers.get("content-type") || "",
  };
}

function buildExistingCharacterMap(characters) {
  const map = new Map();
  for (const row of characters) {
    const keys = new Set([looseKey(row.slug), looseKey(row.display_name)]);
    for (const key of keys) {
      if (key && !map.has(key)) {
        map.set(key, row);
      }
    }
  }
  return map;
}

function nextSortOrder(characters) {
  return characters.reduce((max, row) => Math.max(max, Number(row.sort_order || 0)), 0) + 1;
}

function allocateSlug(baseSlug, existingSlugs) {
  let candidate = baseSlug || "character";
  if (!existingSlugs.has(candidate)) {
    existingSlugs.add(candidate);
    return candidate;
  }

  let index = 2;
  while (existingSlugs.has(`${candidate}-${index}`)) {
    index += 1;
  }
  candidate = `${candidate}-${index}`;
  existingSlugs.add(candidate);
  return candidate;
}

function resolveCharacterTarget(folder, existingMap, existingSlugs) {
  const override = FOLDER_OVERRIDES[folder.id];
  if (override) {
    const existing = existingMap.get(looseKey(override.slug)) || existingMap.get(looseKey(override.display_name)) || null;
    return {
      existing,
      slug: existing?.slug || override.slug,
      display_name: override.display_name,
      created: !existing,
    };
  }

  const key = looseKey(folder.name);
  const existing = existingMap.get(key) || null;
  if (existing) {
    return {
      existing,
      slug: existing.slug,
      display_name: cleanDisplayName(folder.name),
      created: false,
    };
  }

  return {
    existing: null,
    slug: allocateSlug(slugify(folder.name), existingSlugs),
    display_name: cleanDisplayName(folder.name),
    created: true,
  };
}

async function loadCharacterProfile(folder) {
  const topLevel = await fetchFolderChildren(folder.id);
  const photoFolder =
    topLevel.find((item) => item.mime_type === DRIVE_FOLDER_MIME && PHOTO_FOLDER_PATTERN.test(item.name)) ||
    topLevel.find((item) => item.mime_type === DRIVE_FOLDER_MIME && /photo/i.test(item.name)) ||
    null;

  const doc =
    topLevel.find((item) => item.mime_type === DRIVE_DOC_MIME && /lore|info|profile/i.test(item.name)) ||
    topLevel.find((item) => item.mime_type === DRIVE_DOC_MIME) ||
    null;

  const photoItems = photoFolder ? await fetchFolderChildren(photoFolder.id) : topLevel;
  const images = photoItems.filter(isImageItem);
  const profileText = doc ? await fetchDocText(doc.id).catch(() => "") : "";

  return {
    doc,
    images,
    photo_folder: photoFolder,
    profile_text: profileText,
  };
}

async function importImageAsset({ file, characterSlug, sourceFolderName, existingAssetSourceIds }) {
  if (existingAssetSourceIds.has(file.id)) {
    return { skipped: true };
  }

  const originalFilename = decodeDriveString(file.name);
  const ext = path.extname(originalFilename) || mimeToExt(file.mime_type);
  const storedFilename = `${Date.now()}-${crypto.randomUUID()}-${sanitizeFilename(originalFilename)}${ext}`;
  const absolutePath = path.join(DATA_ROOT, "storage", "uploads", "drive-profiles", characterSlug, storedFilename);
  const download = await downloadDriveFile(file.id, absolutePath);
  const relativeUploadPath = path.relative(DATA_ROOT, absolutePath);
  const slot = file.slot;

  const asset = createAsset({
    entity_type: "character",
    entity_slug: characterSlug,
    slot,
    label: SLOT_LABELS[slot] || cleanDisplayName(path.basename(originalFilename, path.extname(originalFilename))),
    original_filename: originalFilename,
    stored_filename: storedFilename,
    relative_upload_path: relativeUploadPath,
    mime_type: file.mime_type || download.mime_type,
    source_type: "drive_import",
    is_public: 0,
    is_approved: 1,
    notes: `Imported from Google Drive profile folder "${sourceFolderName}".`,
    review_status: "approved",
    is_primary: slot === "portrait" ? 1 : 0,
    source_attachment_id: file.id,
    source_url: `https://drive.google.com/file/d/${file.id}/view`,
  });

  existingAssetSourceIds.add(file.id);
  return { skipped: false, asset };
}

async function main() {
  const rootArg = process.argv[2] || DEFAULT_ROOT_URL;
  const rootFolderId = (rootArg.match(/[-\w]{20,}/) || [])[0];
  const onlyKeys = parseOnlyKeys(process.env.SSB_DRIVE_IMPORT_ONLY);
  if (!rootFolderId) {
    throw new Error("A Google Drive folder ID or URL is required.");
  }

  const importRun = SKIP_RUN_LOG
    ? null
    : createImportRun({
        source_type: "drive_import",
        import_slug: "google-drive-character-profiles",
        title: "Google Drive character profile import",
        source_url: `https://drive.google.com/drive/folders/${rootFolderId}`,
        summary: {
          status: "running",
        },
      });

  const existingCharacters = listCharacters();
  const existingAssets = listAssets();
  const existingMap = buildExistingCharacterMap(existingCharacters);
  const existingSlugs = new Set(existingCharacters.map((row) => row.slug));
  const existingAssetSourceIds = new Set(existingAssets.map((row) => row.source_attachment_id).filter(Boolean));
  let sortOrder = nextSortOrder(existingCharacters);

  const summary = {
    root_folder_id: rootFolderId,
    characters_seen: 0,
    characters_created: 0,
    characters_updated: 0,
    portraits_imported: 0,
    assets_imported: 0,
    assets_skipped: 0,
    profiles_with_docs: 0,
    profiles_without_images: [],
    processed: [],
  };

  try {
    let characterFolders = (await fetchFolderChildren(rootFolderId)).filter((item) => item.mime_type === DRIVE_FOLDER_MIME);
    if (onlyKeys.size > 0) {
      characterFolders = characterFolders.filter((folder) => {
        const override = FOLDER_OVERRIDES[folder.id];
        return onlyKeys.has(looseKey(folder.name)) || onlyKeys.has(looseKey(override?.display_name || ""));
      });
    }

    for (const folder of characterFolders) {
      summary.characters_seen += 1;
      const target = resolveCharacterTarget(folder, existingMap, existingSlugs);
      const profile = await loadCharacterProfile(folder);
      const summaryText = pickProfileSummary(profile.profile_text);
      const profileText = profile.profile_text.slice(0, 20000);

      const existing = target.existing;
      const character = upsertCharacter({
        slug: target.slug,
        display_name: target.display_name || existing?.display_name || cleanDisplayName(folder.name),
        roster_group:
          existing?.source_type === "discord_import" || !existing?.roster_group
            ? "Drive Profiles"
            : existing.roster_group,
        archetype: existing?.archetype || "",
        franchise: existing?.franchise || "Anime Fighter Pro",
        tags: mergeTags(existing?.tags, "drive-profile", "google-drive"),
        status: existing?.status && existing.status !== "discord-import" ? existing.status : "concept",
        description: summaryText || existing?.description || "",
        mechanics_notes: existing?.mechanics_notes || "",
        moveset_notes: profileText || existing?.moveset_notes || "",
        unlock_notes: existing?.unlock_notes || "",
        is_public: existing?.is_public ?? 0,
        sort_order: existing?.sort_order || sortOrder++,
        approval_status: existing?.approval_status || "unapproved",
        release_bucket: "Undecided",
        review_status: "approved",
        source_type: "drive_import",
        source_url: `https://drive.google.com/drive/folders/${folder.id}`,
        import_run_id: importRun?.id || 0,
      });

      if (target.created) {
        summary.characters_created += 1;
      } else {
        summary.characters_updated += 1;
      }

      if (profile.profile_text) {
        summary.profiles_with_docs += 1;
      }

      if (!profile.images.length) {
        summary.profiles_without_images.push({
          slug: character.slug,
          display_name: character.display_name,
        });
        summary.processed.push({
          slug: character.slug,
          display_name: character.display_name,
          images: 0,
          created: target.created,
        });
        continue;
      }

      const portrait = choosePortrait(profile.images, character.display_name);
      const slotState = {
        stock_icon: false,
        select_art: false,
        reference_sheet: false,
      };

      const imagesWithSlots = profile.images.map((image) => ({
        ...image,
        slot: classifySlot(image, portrait?.id, slotState),
      }));
      const selectedImages = limitImages(imagesWithSlots);

      let importedForCharacter = 0;
      for (const image of selectedImages) {
        const result = await importImageAsset({
          file: image,
          characterSlug: character.slug,
          sourceFolderName: character.display_name,
          existingAssetSourceIds,
        });
        if (result.skipped) {
          summary.assets_skipped += 1;
          continue;
        }
        importedForCharacter += 1;
        summary.assets_imported += 1;
        if (image.slot === "portrait") {
          summary.portraits_imported += 1;
        }
      }

      summary.processed.push({
        slug: character.slug,
        display_name: character.display_name,
        images: profile.images.length,
        selected_images: selectedImages.length,
        imported_assets: importedForCharacter,
        created: target.created,
      });
      console.log(`[drive-import] ${character.display_name}: ${importedForCharacter}/${selectedImages.length} selected images imported (${profile.images.length} available)`);
    }

    if (importRun) {
      updateImportRun(importRun.id, {
        source_type: "drive_import",
        import_slug: "google-drive-character-profiles",
        title: "Google Drive character profile import",
        source_url: `https://drive.google.com/drive/folders/${rootFolderId}`,
        summary: {
          ...summary,
          status: "completed",
        },
      });
    }

    console.log(JSON.stringify(summary, null, 2));
  } catch (error) {
    if (importRun) {
      try {
        updateImportRun(importRun.id, {
          source_type: "drive_import",
          import_slug: "google-drive-character-profiles",
          title: "Google Drive character profile import",
          source_url: `https://drive.google.com/drive/folders/${rootFolderId}`,
          summary: {
            ...summary,
            status: "failed",
            error: error.message,
          },
        });
      } catch (runLogError) {
        console.error(`[drive-import] unable to update import run: ${runLogError.message}`);
      }
    }
    throw error;
  }
}

await main();
