import fs from "fs";
import path from "path";
import { Api } from "telegram";

import { config } from "../config.js";
import {
  ensureTelegramAuth,
  listDialogs
} from "./telegramClient.js";

import {
  initDb,
  upsertModel,
  upsertSource,
  getEnabledSources,
  updateSourceProgress,
  updateModelThumbnail
} from "./db.js";

fs.mkdirSync(config.thumbnailDir, { recursive: true });

const MODEL_EXTENSIONS =
  /\.(stl|3mf|obj|fbx|blend|glb|gltf|step|stp|iges|igs|zip|rar|7z)$/i;

const PHOTO_WINDOW = 6;

function getExtension(filename = "") {
  return path.extname(filename).toLowerCase();
}

function isModelFile(filename = "") {
  return MODEL_EXTENSIONS.test(filename);
}

function filenameFromDocument(document) {
  const attr = document?.attributes?.find(
    item => item instanceof Api.DocumentAttributeFilename
  );

  return attr?.fileName || "";
}

async function saveThumbFromMessage(
  client,
  message,
  key
) {
  const output = path.join(
    config.thumbnailDir,
    `${key}.jpg`
  );

  try {
    // Telegram/GramJS supports thumb=N to download only a document/photo thumbnail.
    if (
      message?.document?.thumbs?.length ||
      message?.photo?.sizes?.length
    ) {
      const result = await client.downloadMedia(
        message,
        {
          thumb: 0,
          outputFile: output
        }
      );

      if (
        (result === undefined || result === null) &&
        !fs.existsSync(output)
      ) {
        return null;
      }

      return fs.existsSync(output)
        ? `/thumbnails/${path.basename(output)}`
        : null;
    }
  } catch (error) {
    console.warn(
      `Thumbnail failed ${message?.id}:`,
      error.message
    );
  }

  return null;
}

function sameAlbum(a, b) {
  return Boolean(
    a?.groupedId &&
    b?.groupedId &&
    String(a.groupedId) === String(b.groupedId)
  );
}

function nearbyMessage(a, b) {
  return Math.abs(
    Number(a.id) - Number(b.id)
  ) <= PHOTO_WINDOW;
}

async function saveModelRecord({
  client,
  source,
  message,
  filename,
  thumbnailPath = null
}) {
  const key =
    `${String(source.chat_id).replace(/[^0-9_-]/g, "_")}_${message.id}`;

  if (!thumbnailPath) {
    thumbnailPath =
      await saveThumbFromMessage(
        client,
        message,
        key
      );
  }

  await upsertModel({
    chat_id: String(source.chat_id),
    message_id: Number(message.id),
    name: filename || "Modelo",
    description: message.message || "",
    filename,
    extension: getExtension(filename),
    mime_type:
      message.document?.mimeType ||
      "application/octet-stream",
    size_bytes:
      Number(message.document?.size || 0),
    thumbnail_path: thumbnailPath,
    message_date: message.date
      ? new Date(message.date * 1000).toISOString()
      : null
  });

  return thumbnailPath;
}

export async function discoverSources() {
  const dialogs = await listDialogs();

  const sources =
    dialogs.filter(
      dialog =>
        dialog.type === "channel" ||
        dialog.type === "group"
    );

  for (const source of sources) {
    await upsertSource(source);
  }

  return sources;
}

export async function indexSource(
  source,
  maxMessages = 1000,
  forceFullScan = false
) {
  const client =
    await ensureTelegramAuth();

  const entity =
    await client.getEntity(
      source.chat_id
    );

  let count = 0;
  let thumbnailCount = 0;

  let newestId =
    Number(
      source.last_message_id || 0
    );

  const recentPhotos = [];
  const pendingDocuments = [];

  for await (
    const message of
      client.iterMessages(
        entity,
        {
          limit:
            maxMessages > 0
              ? maxMessages
              : undefined
        }
      )
  ) {
    const messageId =
      Number(message.id || 0);

    if (
      !forceFullScan &&
      source.last_message_id &&
      messageId <=
        Number(source.last_message_id)
    ) {
      break;
    }

    if (messageId > newestId) {
      newestId = messageId;
    }

    /*
     * PHOTO MESSAGE
     *
     * It may be the preview for a nearby model file.
     * We keep it briefly and match it without any extra GetMessages call.
     */
    if (message.photo) {
      recentPhotos.push(message);

      while (
        recentPhotos.length > PHOTO_WINDOW * 2
      ) {
        recentPhotos.shift();
      }

      for (
        let i = pendingDocuments.length - 1;
        i >= 0;
        i--
      ) {
        const pending =
          pendingDocuments[i];

        if (
          sameAlbum(
            pending.message,
            message
          ) ||
          nearbyMessage(
            pending.message,
            message
          )
        ) {
          const key =
            `${String(source.chat_id).replace(/[^0-9_-]/g, "_")}_${pending.message.id}`;

          const thumb =
            await saveThumbFromMessage(
              client,
              message,
              key
            );

          await saveModelRecord({
            client,
            source,
            message: pending.message,
            filename: pending.filename,
            thumbnailPath: thumb
          });

          if (thumb) thumbnailCount++;

          pendingDocuments.splice(
            i,
            1
          );
        }
      }

      continue;
    }

    /*
     * MODEL DOCUMENT
     */
    if (!message.document) {
      continue;
    }

    const filename =
      filenameFromDocument(
        message.document
      );

    if (!isModelFile(filename)) {
      continue;
    }

    /*
     * First try a photo already seen nearby.
     * This costs no additional Telegram GetMessages call.
     */
    const existingPhoto =
      recentPhotos
        .find(photo =>
          sameAlbum(
            photo,
            message
          ) ||
          nearbyMessage(
            photo,
            message
          )
        );

    let thumb = null;

    if (existingPhoto) {
      const key =
        `${String(source.chat_id).replace(/[^0-9_-]/g, "_")}_${message.id}`;

      thumb =
        await saveThumbFromMessage(
          client,
          existingPhoto,
          key
        );
    }

    if (thumb) {
      thumbnailCount++;
    }

    await saveModelRecord({
      client,
      source,
      message,
      filename,
      thumbnailPath: thumb
    });

    count++;

    /*
     * If there was no photo on the current side,
     * hold the document briefly so a following older photo
     * can be matched without another API request.
     */
    if (!thumb) {
      pendingDocuments.push({
        message,
        filename
      });

      while (
        pendingDocuments.length >
        PHOTO_WINDOW * 2
      ) {
        pendingDocuments.shift();
      }
    }

    console.log(
      `[INDEX] ${source.title}: ${filename}${thumb ? " + imagen" : ""}`
    );
  }

  /*
   * Persist any unmatched model records.
   * They were already saved; this loop only clears the buffer.
   */
  pendingDocuments.length = 0;

  await updateSourceProgress(
    source.chat_id,
    newestId
  );

  return {
    count,
    thumbnailCount
  };
}

export async function syncAllSources(
  maxMessages = 1000,
  forceFullScan = false
) {
  await initDb();

  const discovered =
    await discoverSources();

  const enabled =
    await getEnabledSources();

  const results = [];

  for (const source of enabled) {
    try {
      const result =
        await indexSource(
          source,
          maxMessages,
          forceFullScan
        );

      results.push({
        chat_id: source.chat_id,
        title: source.title,
        count: result.count,
        thumbnailCount:
          result.thumbnailCount,
        ok: true
      });
    } catch (error) {
      results.push({
        chat_id: source.chat_id,
        title: source.title,
        count: 0,
        thumbnailCount: 0,
        ok: false,
        error: error.message
      });
    }
  }

  return {
    discovered:
      discovered.length,
    indexedSources:
      enabled.length,
    results
  };
}

if (
  process.argv[1]
    ?.endsWith("indexer.js")
) {
  await initDb();

  const maxMessages =
    Number(
      process.argv[2] || 1000
    );

  const full =
    process.argv[3] === "--full";

  const result =
    await syncAllSources(
      maxMessages,
      full
    );

  console.log(
    JSON.stringify(
      result,
      null,
      2
    )
  );

  process.exit(
    result.results.some(
      item => !item.ok
    )
      ? 1
      : 0
  );
}
