import express from "express";
import path from "path";
import crypto from "crypto";
import { fileURLToPath } from "url";

import { config } from "../config.js";
import {
  initDb,
  searchModels,
  getModelById,
  listSources,
  resetSourceProgress,
  setSourceEnabled
,
  createUser,
  findUserByUsername,
  countUsers,
  createWebSession,
  getWebSession,
  deleteWebSession
} from "./db.js";
import {
  listDialogs,
  getMessageById,
  streamMessageToResponse
} from "./telegramClient.js";
import {
  discoverSources,
  syncAllSources
} from "./indexer.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = config.port;
const frontendPath = path.join(config.projectRoot, "frontend");
const adminPath = path.join(config.projectRoot, "frontend-admin");
const adminSessions = new Map();

const ADMIN_PASSWORD = config.adminPassword;
const SYNC_SECRET = process.env.SYNC_SECRET || "";
const SYNC_INTERVAL_HOURS = Math.max(Number(process.env.SYNC_INTERVAL_HOURS || 24), 1);
const SYNC_MESSAGES = Math.min(Math.max(Number(process.env.SYNC_MESSAGES || 500), 50), 5000);

let syncInProgress = false;

app.use(express.json());
app.use(express.static(frontendPath));
app.use("/thumbnails", express.static(config.thumbnailDir));

/* WEB AUTHENTICATION */

const SESSION_TTL_HOURS = Math.max(
  Number(process.env.SESSION_TTL_HOURS || 24),
  1
);

const REGISTRATION_CODE =
  String(process.env.REGISTRATION_CODE || "").trim();

function normalizeUsername(value) {
  return String(value || "")
    .trim()
    .toLowerCase();
}

function hashPassword(password, saltHex = null) {
  const salt =
    saltHex
      ? Buffer.from(saltHex, "hex")
      : crypto.randomBytes(16);

  const derived =
    crypto.scryptSync(
      String(password),
      salt,
      64
    );

  return `scrypt:${salt.toString("hex")}:${derived.toString("hex")}`;
}

function verifyPassword(password, stored) {
  const parts = String(stored || "").split(":");

  if (parts.length !== 3 || parts[0] !== "scrypt") {
    return false;
  }

  const salt = Buffer.from(parts[1], "hex");
  const expected = Buffer.from(parts[2], "hex");

  const actual =
    crypto.scryptSync(
      String(password),
      salt,
      expected.length
    );

  return (
    actual.length === expected.length &&
    crypto.timingSafeEqual(actual, expected)
  );
}

function newSessionToken() {
  return crypto.randomBytes(48).toString("hex");
}

function hashToken(token) {
  return crypto
    .createHash("sha256")
    .update(token)
    .digest("hex");
}

function setWebCookie(res, token, maxAgeSeconds) {
  res.setHeader(
    "Set-Cookie",
    `stl_session=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${maxAgeSeconds}`
  );
}

function clearWebCookie(res) {
  res.setHeader(
    "Set-Cookie",
    "stl_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0"
  );
}

function getWebToken(req) {
  const cookie = req.headers.cookie || "";
  const match =
    cookie.match(/(?:^|;\s*)stl_session=([^;]+)/);

  return match
    ? decodeURIComponent(match[1])
    : null;
}

async function currentUser(req) {
  const rawToken = getWebToken(req);

  if (!rawToken) {
    return null;
  }

  return getWebSession(
    hashToken(rawToken)
  );
}

async function requireUser(req, res, next) {
  try {
    const user = await currentUser(req);

    if (!user) {
      return res.status(401).json({
        ok: false,
        error: "Debes iniciar sesión."
      });
    }

    req.user = user;
    next();
  } catch (error) {
    console.error("AUTH ERROR:", error);

    res.status(500).json({
      ok: false,
      error: "No se pudo validar la sesión."
    });
  }
}

/* AUTH API */

app.get("/api/auth/me", async (req, res) => {
  try {
    const user = await currentUser(req);

    res.json({
      ok: true,
      authenticated: Boolean(user),
      user: user
        ? {
            username: user.username,
            role: user.role
          }
        : null
    });
  } catch (error) {
    res.status(500).json({
      ok: false,
      error: "No se pudo consultar la sesión."
    });
  }
});

app.post("/api/auth/register", async (req, res) => {
  try {
    if (!REGISTRATION_CODE) {
      return res.status(503).json({
        ok: false,
        error: "El registro está deshabilitado."
      });
    }

    const username =
      normalizeUsername(req.body.username);

    const password =
      String(req.body.password || "");

    const code =
      String(req.body.registrationCode || "").trim();

    if (!/^[a-z0-9._-]{3,32}$/.test(username)) {
      return res.status(400).json({
        ok: false,
        error: "Usuario inválido."
      });
    }

    if (password.length < 8) {
      return res.status(400).json({
        ok: false,
        error: "La contraseña debe tener al menos 8 caracteres."
      });
    }

    if (code !== REGISTRATION_CODE) {
      return res.status(403).json({
        ok: false,
        error: "Código de registro incorrecto."
      });
    }

    const existing =
      await findUserByUsername(username);

    if (existing) {
      return res.status(409).json({
        ok: false,
        error: "Ese usuario ya existe."
      });
    }

    const userCount =
      await countUsers();

    const role =
      userCount === 0
        ? "owner"
        : "member";

    const created =
      await createUser({
        username,
        passwordHash:
          hashPassword(password),
        role
      });

    const token =
      newSessionToken();

    const expiresAt =
      Date.now() +
      SESSION_TTL_HOURS *
      60 * 60 * 1000;

    await createWebSession({
      tokenHash:
        hashToken(token),
      userId:
        created.id,
      expiresAt
    });

    setWebCookie(
      res,
      token,
      SESSION_TTL_HOURS * 60 * 60
    );

    res.json({
      ok: true,
      user: {
        username,
        role
      }
    });
  } catch (error) {
    console.error("REGISTER ERROR:", error);

    res.status(500).json({
      ok: false,
      error: "No se pudo crear la cuenta."
    });
  }
});

app.post("/api/auth/login", async (req, res) => {
  try {
    const username =
      normalizeUsername(req.body.username);

    const password =
      String(req.body.password || "");

    const user =
      await findUserByUsername(username);

    if (
      !user ||
      !verifyPassword(
        password,
        user.password_hash
      )
    ) {
      return res.status(401).json({
        ok: false,
        error: "Usuario o contraseña incorrectos."
      });
    }

    const token =
      newSessionToken();

    const expiresAt =
      Date.now() +
      SESSION_TTL_HOURS *
      60 * 60 * 1000;

    await createWebSession({
      tokenHash:
        hashToken(token),
      userId:
        user.id,
      expiresAt
    });

    setWebCookie(
      res,
      token,
      SESSION_TTL_HOURS * 60 * 60
    );

    res.json({
      ok: true,
      user: {
        username: user.username,
        role: user.role
      }
    });
  } catch (error) {
    console.error("LOGIN ERROR:", error);

    res.status(500).json({
      ok: false,
      error: "No se pudo iniciar sesión."
    });
  }
});

app.post("/api/auth/logout", async (req, res) => {
  try {
    const token = getWebToken(req);

    if (token) {
      await deleteWebSession(
        hashToken(token)
      );
    }

    clearWebCookie(res);

    res.json({
      ok: true
    });
  } catch (error) {
    res.status(500).json({
      ok: false,
      error: "No se pudo cerrar la sesión."
    });
  }
});

/* PUBLIC CATALOG */

app.get("/api/catalog/search", requireUser, async (req, res) => {
  try {
    const query = String(req.query.q || "").trim();
    const limit = Math.min(
      Math.max(Number(req.query.limit || 50), 1),
      100
    );

    const results = await searchModels(query, limit);

    res.json({
      ok: true,
      count: results.length,
      results
    });
  } catch (error) {
    console.error("SEARCH ERROR:", error);

    res.status(500).json({
      ok: false,
      error: "No se pudo consultar la biblioteca."
    });
  }
});

app.get("/api/catalog/status", requireUser, (_req, res) => {
  res.json({
    ok: true,
    status: "online",
    syncing: syncInProgress
  });
});

app.get("/api/catalog/download/:id", requireUser, async (req, res) => {
  try {
    const model = await getModelById(req.params.id);

    if (!model) {
      return res.status(404).json({
        ok: false,
        error: "Modelo no encontrado."
      });
    }

    const message = await getMessageById(
      model.chat_id,
      model.message_id
    );

    console.log(
      `[DOWNLOAD] model=${model.id} message=${model.message_id} filename=${model.filename}`
    );

    if (!message?.document) {
      return res.status(404).json({
        ok: false,
        error: "El archivo original ya no está disponible."
      });
    }

    await streamMessageToResponse(
      message,
      res,
      model.filename || model.name || "modelo",
      model.mime_type || "application/octet-stream",
      model.size_bytes || 0
    );
  } catch (error) {
    console.error("DOWNLOAD ERROR:", error);

    if (!res.headersSent) {
      res.status(500).json({
        ok: false,
        error: "No se pudo descargar el modelo."
      });
    } else {
      res.destroy(error);
    }
  }
});

/* SCHEDULED SYNC */

function authorizedSyncRequest(req) {
  if (!SYNC_SECRET) return false;

  const headerSecret = String(req.headers["x-sync-secret"] || "");
  const querySecret = String(req.query.secret || "");
  const candidate = headerSecret || querySecret;

  return candidate === SYNC_SECRET;
}

async function runIncrementalSync() {
  if (syncInProgress) {
    return {
      ok: true,
      skipped: true,
      reason: "sync_in_progress"
    };
  }

  syncInProgress = true;

  try {
    const result = await syncAllSources(
      SYNC_MESSAGES,
      false
    );

    console.log(
      "[SCHEDULED SYNC]",
      JSON.stringify(result)
    );

    return {
      ok: true,
      result
    };
  } finally {
    syncInProgress = false;
  }
}

/*
 * Hosting cron can call this endpoint once every 12/24 hours.
 * No Telegram information is returned.
 */
app.post("/api/internal/sync", async (req, res) => {
  if (!authorizedSyncRequest(req)) {
    return res.status(401).json({
      ok: false,
      error: "Unauthorized."
    });
  }

  try {
    const result = await runIncrementalSync();

    res.json(result);
  } catch (error) {
    console.error("[CRON SYNC ERROR]", error);

    res.status(500).json({
      ok: false,
      error: "Synchronization failed."
    });
  }
});

/*
 * Optional health endpoint for hosting monitors.
 */
app.get("/api/internal/health", (_req, res) => {
  res.json({
    ok: true,
    status: "online",
    syncIntervalHours: SYNC_INTERVAL_HOURS
  });
});

/* ADMIN */

function getAdminToken(req) {
  const cookie = req.headers.cookie || "";
  const match = cookie.match(/(?:^|;\s*)ml_admin=([^;]+)/);
  return match ? decodeURIComponent(match[1]) : null;
}

function requireAdmin(req, res, next) {
  const token = getAdminToken(req);

  if (!token || !adminSessions.has(token)) {
    return res.status(401).json({
      ok: false,
      error: "No autorizado."
    });
  }

  next();
}

app.post("/api/admin/login", (req, res) => {
  if (!ADMIN_PASSWORD) {
    return res.status(500).json({
      ok: false,
      error: "ADMIN_PASSWORD no está configurada."
    });
  }

  const password = String(req.body.password || "");
  const a = Buffer.from(password);
  const b = Buffer.from(ADMIN_PASSWORD);

  const valid =
    a.length === b.length &&
    crypto.timingSafeEqual(a, b);

  if (!valid) {
    return res.status(401).json({
      ok: false,
      error: "Contraseña incorrecta."
    });
  }

  const token = crypto.randomBytes(32).toString("hex");

  adminSessions.set(token, {
    createdAt: Date.now()
  });

  res.setHeader(
    "Set-Cookie",
    `ml_admin=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800`
  );

  res.json({ ok: true });
});

app.post("/api/admin/logout", requireAdmin, (req, res) => {
  const token = getAdminToken(req);
  if (token) adminSessions.delete(token);

  res.setHeader(
    "Set-Cookie",
    "ml_admin=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0"
  );

  res.json({ ok: true });
});

app.get("/api/admin/status", requireAdmin, (_req, res) => {
  res.json({
    ok: true,
    authenticated: true,
    syncing: syncInProgress
  });
});

/*
 * Discover all channels/groups automatically.
 */
app.post("/api/admin/sources/refresh", requireAdmin, async (_req, res) => {
  try {
    const sources = await discoverSources();

    res.json({
      ok: true,
      count: sources.length,
      sources: await listSources()
    });
  } catch (error) {
    console.error("SOURCE DISCOVERY ERROR:", error);

    res.status(500).json({
      ok: false,
      error: error.message
    });
  }
});

app.get("/api/admin/sources", requireAdmin, async (_req, res) => {
  try {
    // Keep discovery automatic each time the admin opens/refeshes.
    await discoverSources();

    res.json({
      ok: true,
      sources: await listSources()
    });
  } catch (error) {
    console.error("SOURCES ERROR:", error);

    res.status(500).json({
      ok: false,
      error: error.message
    });
  }
});

app.post("/api/admin/sources/:chatId/enabled", requireAdmin, async (req, res) => {
  try {
    await setSourceEnabled(
      req.params.chatId,
      Boolean(req.body.enabled)
    );

    res.json({ ok: true });
  } catch (error) {
    res.status(500).json({
      ok: false,
      error: error.message
    });
  }
});

/*
 * Index every enabled channel/group.
 * No source names are returned by the public catalog.
 */
app.post("/api/admin/index-all", requireAdmin, async (req, res) => {
  if (syncInProgress) {
    return res.json({
      ok: true,
      message: "Ya hay una sincronización en curso."
    });
  }

  const maxMessages = Math.min(
    Math.max(Number(req.body.maxMessages || 1000), 1),
    5000
  );

  const forceFullScan =
    Boolean(req.body.forceFullScan);

  syncInProgress = true;

  void syncAllSources(
    maxMessages,
    forceFullScan
  )
    .then(result => {
      console.log(
        "[GLOBAL SYNC]",
        JSON.stringify(result)
      );
    })
    .catch(error => {
      console.error(
        "[GLOBAL SYNC ERROR]",
        error
      );
    })
    .finally(() => {
      syncInProgress = false;
    });

  res.json({
    ok: true,
    message: forceFullScan
      ? "Reindexación completa iniciada."
      : "Indexación incremental iniciada."
  });
});

app.post("/api/admin/repair-thumbnails", requireAdmin, async (req, res) => {
  if (syncInProgress) {
    return res.json({
      ok: true,
      message: "Ya hay una sincronización en curso."
    });
  }

  const maxMessages = Math.min(
    Math.max(Number(req.body.maxMessages || 500), 1),
    5000
  );

  syncInProgress = true;

  void syncAllSources(
    maxMessages,
    true
  )
    .then(result => {
      console.log(
        "[THUMBNAIL REPAIR]",
        JSON.stringify(result)
      );
    })
    .catch(error => {
      console.error(
        "[THUMBNAIL REPAIR ERROR]",
        error
      );
    })
    .finally(() => {
      syncInProgress = false;
    });

  res.json({
    ok: true,
    message:
      "Reparación de miniaturas iniciada. Se volverán a revisar las fuentes."
  });
});

/*
 * Compatibility endpoint: manually index one source.
 */
app.post("/api/admin/index", requireAdmin, async (req, res) => {
  try {
    const chatId = String(req.body.chatId || "").trim();
    if (!chatId) {
      return res.status(400).json({
        ok: false,
        error: "Selecciona una fuente."
      });
    }

    const source = (await listSources()).find(
      item => String(item.chat_id) === chatId
    );

    if (!source) {
      return res.status(404).json({
        ok: false,
        error: "Fuente no encontrada."
      });
    }

    // Hand the single-source case to the same global worker through a one-source flag.
    res.json({
      ok: false,
      error: "Usa Indexar todas las fuentes en esta versión."
    });
  } catch (error) {
    res.status(500).json({
      ok: false,
      error: error.message
    });
  }
});

/* ADMIN UI */

app.get("/admin", (_req, res) => {
  res.sendFile(path.join(adminPath, "index.html"));
});

app.use("/admin-assets", express.static(adminPath));

app.use(async (req, res, next) => {
  if (
    req.method !== "GET" ||
    req.path.startsWith("/api/") ||
    req.path.startsWith("/thumbnails/") ||
    req.path.startsWith("/admin")
  ) {
    return next();
  }

  /*
   * Do not accidentally expose the catalog.
   * The browser receives the app shell, which immediately redirects
   * to the login/register view when there is no authenticated session.
   */
  res.sendFile(
    path.join(
      frontendPath,
      "index.html"
    )
  );
});

app.use((req, res) => {
  res.status(404).json({
    ok: false,
    error: "Ruta no encontrada."
  });
});

async function scheduledInProcessSync() {
  if (syncInProgress) return;

  try {
    await runIncrementalSync();
  } catch (error) {
    console.error(
      "[BACKGROUND SYNC ERROR]",
      error.message
    );
  }
}


async function start() {
  try {
    await initDb();

    console.log("");
    console.log("======================================");
    console.log(" PRIVATE MODEL LIBRARY V6");
    console.log(" GLOBAL INDEX ENABLED");
    console.log("======================================");
    console.log(
      "ADMIN_PASSWORD:",
      ADMIN_PASSWORD ? "CONFIGURADA" : "NO CONFIGURADA"
    );
    console.log(
      "TELEGRAM_SESSION:",
      config.telegramSession ? "CONFIGURADA" : "NO CONFIGURADA"
    );

    app.listen(PORT, () => {
      console.log(`Web:   http://localhost:${PORT}`);
      console.log(`Admin: http://localhost:${PORT}/admin`);
      console.log("Public catalog: AUTH REQUIRED");
      console.log("Registration: INVITE CODE REQUIRED");
      console.log(`Sync: every ${SYNC_INTERVAL_HOURS} hour(s), ${SYNC_MESSAGES} messages/source`);
      console.log("");
    });

    /*
     * If the Node process remains alive on the hosting server, keep an
     * in-process fallback scheduler. On shared hosting, prefer the
     * /api/internal/sync endpoint from the hosting Cron Jobs feature.
     */
    setTimeout(
      scheduledInProcessSync,
      10 * 1000
    );

    setInterval(
      scheduledInProcessSync,
      SYNC_INTERVAL_HOURS * 60 * 60 * 1000
    );
  } catch (error) {
    console.error("No se pudo iniciar:", error);
    process.exit(1);
  }
}

start();
