import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import {
  APP_ROOT,
  DATA_ROOT,
  createImportRun,
  updateImportRun,
  upsertImportedAsset,
  upsertImportedCharacter,
} from "./db.js";

const DEFAULT_GUILD_ID = "1472850438306140295";
const DEFAULT_FORUM_CHANNEL_ID = "1478516538855391383";
const DEFAULT_IMPORT_SLUG = "character-templates-forum";
const DISCORD_API_ROOT = "https://discord.com/api/v10";
const USER_AGENT = "Mozilla/5.0";

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

function ensureDir(dirPath) {
  fs.mkdirSync(dirPath, { recursive: true });
  return dirPath;
}

function readTokenFromBrave() {
  const script = `
tell application "Brave Browser"
  activate
  execute active tab of front window javascript "JSON.parse(window.localStorage.token)"
end tell
`;
  return execFileSync("osascript", ["-e", script], { encoding: "utf8" }).trim();
}

async function sleep(ms) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchWithRetry(url, { token, responseType = "json" } = {}) {
  for (;;) {
    const response = await fetch(url, {
      headers: {
        "content-type": "application/json",
        authorization: token,
        "user-agent": USER_AGENT,
      },
    });

    if (response.status === 429) {
      let retryAfterMs = 1000;
      try {
        const payload = await response.json();
        retryAfterMs = Math.ceil(Number(payload.retry_after || 1) * 1000);
      } catch {
        retryAfterMs = 1000;
      }
      await sleep(retryAfterMs);
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Discord request failed (${response.status}) for ${url}\n${body.slice(0, 800)}`);
    }

    if (responseType === "buffer") {
      return Buffer.from(await response.arrayBuffer());
    }

    return response.json();
  }
}

function attachmentBucket(attachment, messageIndex) {
  const filename = String(attachment.filename || "").toLowerCase();
  const contentType = String(attachment.content_type || "").toLowerCase();
  const width = Number(attachment.width || 0);
  const height = Number(attachment.height || 0);
  const isVideo = contentType.startsWith("video/") || /\.(mp4|mov|webm|m4v)$/i.test(filename);
  const isImage =
    contentType.startsWith("image/") || /\.(png|jpg|jpeg|webp|gif|jfif)$/i.test(filename);

  if (isVideo) return "motion";
  if (!isImage) return "misc";
  if (/(sheet|reference|ref|turnaround|model|fullbody|profile|charsheet|concept|lineup)/i.test(filename)) return "reference";
  if (/(avatar|icon|pfp|portrait|headshot)/i.test(filename)) return "portrait";
  if (/(screenshot|gallery|download|meme)/i.test(filename)) return "supporting";
  if (messageIndex <= 1 && width >= 900 && height >= 900) return "hero-candidates";
  if (width >= 1000 || height >= 1000) return "hero-candidates";
  return "supporting";
}

function attachmentScore(attachment, bucket, messageIndex) {
  const filename = String(attachment.filename || "").toLowerCase();
  const width = Number(attachment.width || 0);
  const height = Number(attachment.height || 0);
  let score = 0;

  if (bucket === "hero-candidates") score += 60;
  if (bucket === "reference") score += 50;
  if (bucket === "portrait") score += 40;
  if (bucket === "motion") score += 20;
  if (width >= 1000) score += 12;
  if (height >= 1000) score += 12;
  if (messageIndex === 0) score += 10;
  if (/(sheet|reference|ref|model|fullbody|profile|charsheet)/i.test(filename)) score += 15;
  if (/(screenshot|gallery|download|meme)/i.test(filename)) score -= 15;

  return score;
}

function normalizeExt(filename, contentType) {
  const explicit = path.extname(filename || "");
  if (explicit) return explicit.toLowerCase();
  const content = String(contentType || "").toLowerCase();
  if (content.includes("png")) return ".png";
  if (content.includes("jpeg") || content.includes("jpg")) return ".jpg";
  if (content.includes("webp")) return ".webp";
  if (content.includes("gif")) return ".gif";
  if (content.includes("mp4")) return ".mp4";
  if (content.includes("quicktime") || content.includes("mov")) return ".mov";
  return "";
}

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

async function fetchForumSearchPages({ token, guildId, forumChannelId, rawApiDir }) {
  const messages = [];
  const threads = new Map();
  let offset = 0;
  let totalResults = 0;

  for (;;) {
    const url =
      `${DISCORD_API_ROOT}/guilds/${guildId}/messages/search?channel_id=${forumChannelId}` +
      `&include_nsfw=true&offset=${offset}`;
    const payload = await fetchWithRetry(url, { token });
    totalResults = payload.total_results || totalResults;

    fs.writeFileSync(
      path.join(rawApiDir, `search-offset-${String(offset).padStart(4, "0")}.json`),
      JSON.stringify(payload, null, 2)
    );

    for (const thread of payload.threads || []) {
      threads.set(thread.id, thread);
    }

    const flattened = (payload.messages || []).flat().filter(Boolean);
    messages.push(...flattened);

    if (!payload.messages?.length || offset + payload.messages.length >= totalResults) {
      break;
    }

    offset += payload.messages.length;
    await sleep(350);
  }

  return { totalResults, starterMessages: messages, threadsById: threads };
}

async function fetchChannel(token, channelId) {
  return fetchWithRetry(`${DISCORD_API_ROOT}/channels/${channelId}`, { token });
}

async function fetchThreadMessages(token, threadId) {
  const messages = [];
  let before = "";

  for (;;) {
    const url = `${DISCORD_API_ROOT}/channels/${threadId}/messages?limit=100${before ? `&before=${before}` : ""}`;
    const batch = await fetchWithRetry(url, { token });
    if (!Array.isArray(batch) || batch.length === 0) break;
    messages.push(...batch);
    if (batch.length < 100) break;
    before = batch[batch.length - 1].id;
    await sleep(300);
  }

  return messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
}

async function downloadIfMissing({ url, destinationPath, token }) {
  if (fs.existsSync(destinationPath)) return false;
  const buffer = await fetchWithRetry(url, { token, responseType: "buffer" });
  fs.writeFileSync(destinationPath, buffer);
  return true;
}

function starterSummary(thread, starterMessage, threadMessages) {
  const textBlocks = [];
  for (const message of threadMessages) {
    const content = String(message.content || "").trim();
    if (!content) continue;
    textBlocks.push(content);
    if (textBlocks.join("\n\n").length > 1000) break;
  }

  const summaryText = textBlocks.join("\n\n").slice(0, 1200);
  return {
    source_url: `https://discord.com/channels/${thread.guild_id}/${thread.id}`,
    starter_author: starterMessage?.author?.global_name || starterMessage?.author?.username || "",
    starter_excerpt: String(starterMessage?.content || "").trim().slice(0, 500),
    notes_excerpt: summaryText,
  };
}

async function main() {
  const guildId = process.env.DISCORD_GUILD_ID || DEFAULT_GUILD_ID;
  const forumChannelId = process.env.DISCORD_FORUM_CHANNEL_ID || DEFAULT_FORUM_CHANNEL_ID;
  const token = process.env.DISCORD_TOKEN || readTokenFromBrave();
  const importRoot = ensureDir(path.join(DATA_ROOT, "imports", "discord", DEFAULT_IMPORT_SLUG));
  const rawDir = ensureDir(path.join(importRoot, "raw"));
  const rawApiDir = ensureDir(path.join(rawDir, "api"));
  const manifestsDir = ensureDir(path.join(importRoot, "manifests"));
  const organizedDir = ensureDir(path.join(importRoot, "organized", "characters"));

  const run = createImportRun({
    source_type: "discord_import",
    import_slug: DEFAULT_IMPORT_SLUG,
    title: "Discord Character Templates Intake",
    source_guild_id: guildId,
    source_channel_id: forumChannelId,
    source_url: `https://discord.com/channels/${guildId}/${forumChannelId}`,
    summary: { state: "running" },
  });

  const { totalResults, starterMessages, threadsById } = await fetchForumSearchPages({
    token,
    guildId,
    forumChannelId,
    rawApiDir,
  });

  const threadIds = [...new Set(starterMessages.map((message) => message.channel_id))];
  const importSummary = {
    imported_at: new Date().toISOString(),
    guild_id: guildId,
    forum_channel_id: forumChannelId,
    total_search_results: totalResults,
    total_threads: threadIds.length,
    import_run_id: run.id,
    characters: [],
  };

  let globalDownloadCount = 0;
  let globalReuseCount = 0;
  let upsertedAssets = 0;
  let upsertedCharacters = 0;

  for (let index = 0; index < threadIds.length; index += 1) {
    const threadId = threadIds[index];
    let thread = threadsById.get(threadId);
    if (!thread?.name) {
      thread = await fetchChannel(token, threadId);
    }

    const threadMessages = await fetchThreadMessages(token, threadId);
    const starterMessage = threadMessages[0] || starterMessages.find((message) => message.channel_id === threadId);
    const displayName = String(thread?.name || starterMessage?.content || `thread-${threadId}`).trim();
    const slug = slugify(displayName || threadId);

    const characterDir = ensureDir(path.join(organizedDir, slug));
    const bucketDirs = {
      "hero-candidates": ensureDir(path.join(characterDir, "hero-candidates")),
      reference: ensureDir(path.join(characterDir, "reference")),
      portrait: ensureDir(path.join(characterDir, "portrait")),
      supporting: ensureDir(path.join(characterDir, "supporting")),
      motion: ensureDir(path.join(characterDir, "motion")),
      misc: ensureDir(path.join(characterDir, "misc")),
    };

    const summary = starterSummary({ ...thread, guild_id: guildId, id: threadId }, starterMessage, threadMessages);
    const assets = [];
    const messageSummaries = [];

    for (let messageIndex = 0; messageIndex < threadMessages.length; messageIndex += 1) {
      const message = threadMessages[messageIndex];
      messageSummaries.push({
        id: message.id,
        timestamp: message.timestamp,
        author: message.author?.global_name || message.author?.username || "",
        content: String(message.content || "").trim(),
        attachment_count: (message.attachments || []).length,
      });

      for (let attachmentIndex = 0; attachmentIndex < (message.attachments || []).length; attachmentIndex += 1) {
        const attachment = message.attachments[attachmentIndex];
        const bucket = attachmentBucket(attachment, messageIndex);
        const score = attachmentScore(attachment, bucket, messageIndex);
        const ext = normalizeExt(attachment.filename, attachment.content_type);
        const stem = cleanFilenameStem(attachment.filename);
        const safeName = `${String(messageIndex + 1).padStart(3, "0")}-${String(attachmentIndex + 1).padStart(2, "0")}-${stem}${ext}`;
        const fullPath = path.join(bucketDirs[bucket], safeName);
        const downloaded = await downloadIfMissing({ url: attachment.url, destinationPath: fullPath, token });
        if (downloaded) globalDownloadCount += 1;
        else globalReuseCount += 1;

        const relativeUploadPath = path.relative(DATA_ROOT, fullPath);
        assets.push({
          id: attachment.id,
          filename: attachment.filename,
          stored_filename: safeName,
          relative_path: relativeUploadPath,
          bucket,
          score,
          message_id: message.id,
          width: attachment.width || 0,
          height: attachment.height || 0,
          size: attachment.size || 0,
          content_type: attachment.content_type || "",
          url: attachment.url,
          author: message.author?.global_name || message.author?.username || "",
          timestamp: message.timestamp,
          caption: String(message.content || "").trim(),
        });
      }
    }

    assets.sort((a, b) => b.score - a.score || a.filename.localeCompare(b.filename));
    const counts = {
      messages: threadMessages.length,
      assets: assets.length,
      hero_candidates: assets.filter((asset) => asset.bucket === "hero-candidates").length,
      reference: assets.filter((asset) => asset.bucket === "reference").length,
      motion: assets.filter((asset) => asset.bucket === "motion").length,
    };
    const manifest = {
      id: threadId,
      slug,
      display_name: displayName,
      source: {
        guild_id: guildId,
        forum_channel_id: forumChannelId,
        thread_id: threadId,
        url: summary.source_url,
      },
      summary,
      counts,
      top_assets: assets.slice(0, 12),
      assets,
      messages: messageSummaries,
    };

    fs.writeFileSync(path.join(characterDir, "manifest.json"), JSON.stringify(manifest, null, 2));

    upsertImportedCharacter({
      slug,
      display_name: displayName,
      roster_group: "Discord Intake",
      archetype: "",
      franchise: "Anime Fighter Pro",
      tags: "discord,character-template,vtuber,intake",
      status: "discord-import",
      description: summary.starter_excerpt || summary.notes_excerpt.slice(0, 500),
      mechanics_notes: "",
      moveset_notes: summary.notes_excerpt,
      unlock_notes: `Source thread: ${summary.source_url}`,
      is_public: 0,
      sort_order: 100 + index,
      review_status: "pending_review",
      source_guild_id: guildId,
      source_channel_id: forumChannelId,
      source_thread_id: threadId,
      source_message_id: starterMessage?.id || "",
      source_url: summary.source_url,
      import_run_id: run.id,
    });
    upsertedCharacters += 1;

    for (const asset of assets) {
      upsertImportedAsset({
        entity_type: "character",
        entity_slug: slug,
        slot: "",
        label: "",
        original_filename: asset.filename,
        stored_filename: asset.stored_filename,
        relative_upload_path: asset.relative_path,
        mime_type: asset.content_type,
        source_type: "discord_import",
        is_public: 0,
        is_approved: 0,
        notes: asset.caption,
        review_status: "pending_review",
        quality_bucket: asset.bucket,
        quality_score: asset.score,
        is_primary: 0,
        source_guild_id: guildId,
        source_channel_id: forumChannelId,
        source_thread_id: threadId,
        source_message_id: asset.message_id,
        source_attachment_id: asset.id,
        source_url: asset.url,
        import_run_id: run.id,
      });
      upsertedAssets += 1;
    }

    importSummary.characters.push({
      slug,
      display_name: displayName,
      source_url: summary.source_url,
      counts,
      top_asset_paths: manifest.top_assets.slice(0, 5).map((asset) => asset.relative_path),
    });
  }

  fs.writeFileSync(path.join(manifestsDir, "character-roster.json"), JSON.stringify(importSummary, null, 2));
  fs.writeFileSync(
    path.join(manifestsDir, "character-roster.csv"),
    [
      "slug,display_name,source_url,messages,assets,hero_candidates,reference,motion",
      ...importSummary.characters.map((entry) =>
        [
          entry.slug,
          JSON.stringify(entry.display_name),
          entry.source_url,
          entry.counts.messages,
          entry.counts.assets,
          entry.counts.hero_candidates,
          entry.counts.reference,
          entry.counts.motion,
        ].join(",")
      ),
    ].join("\n")
  );

  updateImportRun(run.id, {
    source_type: "discord_import",
    import_slug: DEFAULT_IMPORT_SLUG,
    title: "Discord Character Templates Intake",
    source_guild_id: guildId,
    source_channel_id: forumChannelId,
    source_url: `https://discord.com/channels/${guildId}/${forumChannelId}`,
    summary: {
      ...importSummary,
      downloaded_assets: globalDownloadCount,
      reused_assets: globalReuseCount,
      upserted_assets: upsertedAssets,
      upserted_characters: upsertedCharacters,
      app_root: APP_ROOT,
      data_root: DATA_ROOT,
    },
  });

  console.log(JSON.stringify({
    ok: true,
    total_threads: threadIds.length,
    total_characters_upserted: upsertedCharacters,
    total_assets_upserted: upsertedAssets,
    downloaded_assets: globalDownloadCount,
    reused_assets: globalReuseCount,
    manifest_path: path.join(manifestsDir, "character-roster.json"),
  }, null, 2));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
