import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import express from "express";
import multer from "multer";
import {
  APP_ROOT,
  DATA_ROOT,
  createAsset,
  createMovesetMedia,
  createSubmission,
  deleteAsset,
  deleteCharacter,
  deleteMechanic,
  deleteMovesetMedia,
  deleteSubmission,
  deleteStage,
  getAdminSnapshot,
  getAssetById,
  getMovesetMediaById,
  getPublicAssetById,
  getPublicCatalog,
  getSubmissionById,
  listCharacters,
  listMovesetEntries,
  listMovesetMedia,
  updateAssetReview,
  updateCharacterReview,
  updateSubmission,
  upsertMovesetEntry,
  upsertCharacter,
  upsertMechanic,
  upsertStage,
} from "./db.js";
import { runApplyToGodot } from "./apply-to-godot.js";

const DEFAULT_PORT = Number.parseInt(process.env.PORT || "4310", 10);
const PUBLIC_ROOT = path.join(APP_ROOT, "public");
const UPLOAD_ROOT = path.join(DATA_ROOT, "storage", "uploads");

const sanitizeFilename = (value) =>
  String(value || "file")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9._-]+/g, "-")
    .replace(/-+/g, "-");

const toBool = (value) => {
  if (typeof value === "boolean") {
    return value;
  }
  const normalized = String(value ?? "").trim().toLowerCase();
  return normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes";
};

const uploadFor = (subfolder) =>
  multer({
    storage: multer.diskStorage({
      destination: (_req, _file, callback) => {
        const destination = path.join(UPLOAD_ROOT, subfolder);
        fs.mkdirSync(destination, { recursive: true });
        callback(null, destination);
      },
      filename: (_req, file, callback) => {
        const ext = path.extname(file.originalname);
        const stem = path.basename(file.originalname, ext);
        const finalName = `${Date.now()}-${crypto.randomUUID()}-${sanitizeFilename(stem)}${ext}`;
        callback(null, finalName);
      },
    }),
  });

const adminUpload = uploadFor("admin");
const movesetUpload = uploadFor("moveset-media");
const submissionUpload = uploadFor("submissions");

function assetWithUrl(asset) {
  return {
    ...asset,
    file_url: `/api/files/assets/${asset.id}`,
  };
}

function publicAssetWithUrl(asset) {
  return {
    ...asset,
    file_url: `/api/public/assets/${asset.id}`,
  };
}

function submissionWithUrl(submission) {
  return {
    ...submission,
    file_url: submission.file_relative_path ? `/api/files/submissions/${submission.id}` : "",
  };
}

function movesetMediaWithUrl(asset) {
  return {
    ...asset,
    file_url: `/api/files/moveset-media/${asset.id}`,
  };
}

function removeFileIfExists(filePath) {
  if (filePath && fs.existsSync(filePath)) {
    fs.rmSync(filePath, { force: true });
  }
}

function adminSnapshot() {
  const snapshot = getAdminSnapshot();
  return {
    ...snapshot,
    assets: snapshot.assets.map(assetWithUrl),
    submissions: snapshot.submissions.map(submissionWithUrl),
    apply_runs: snapshot.apply_runs.map((run) => ({
      ...run,
      summary: JSON.parse(run.summary_json),
    })),
    import_runs: snapshot.import_runs.map((run) => ({
      ...run,
      summary: JSON.parse(run.summary_json),
    })),
  };
}

function publicCatalog() {
  const catalog = getPublicCatalog();
  return {
    ...catalog,
    assets: catalog.assets.map(publicAssetWithUrl),
  };
}

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

function uniqueCharacterSlug(displayName) {
  const base = slugifyLocal(displayName) || "character";
  const taken = new Set(listCharacters().map((row) => row.slug));
  if (!taken.has(base)) {
    return base;
  }
  let index = 2;
  while (taken.has(`${base}-${index}`)) {
    index += 1;
  }
  return `${base}-${index}`;
}

function notesSnapshot() {
  return {
    characters: listCharacters().map((row) => ({
      slug: row.slug,
      display_name: row.display_name,
      tags: row.tags || "",
      roster_group: row.roster_group,
      approval_status: row.approval_status || "unapproved",
      release_bucket: row.release_bucket || "unassigned",
      unlock_notes: row.unlock_notes || "",
      source_type: row.source_type,
      review_status: row.review_status,
      is_public: row.is_public,
      sort_order: row.sort_order,
    })),
    entries: listMovesetEntries(),
    media: listMovesetMedia().map(movesetMediaWithUrl),
  };
}

export function createServerApp() {
  const app = express();

  app.use(express.json({ limit: "10mb" }));
  app.use(express.urlencoded({ extended: true, limit: "10mb" }));
  app.use(express.static(PUBLIC_ROOT));

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

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

  app.get("/public", (_req, res) => {
    res.sendFile(path.join(PUBLIC_ROOT, "showcase.html"));
  });

  app.get("/submit", (_req, res) => {
    res.sendFile(path.join(PUBLIC_ROOT, "submit.html"));
  });

  app.get("/notes", (_req, res) => {
    res.sendFile(path.join(PUBLIC_ROOT, "notes.html"));
  });

  app.get("/api/health", (_req, res) => {
    res.json({ ok: true, service: "anime-fighter-pro-organizer" });
  });

  app.get("/api/admin/bootstrap", (_req, res) => {
    res.json(adminSnapshot());
  });

  app.get("/api/notes/bootstrap", (_req, res) => {
    res.json(notesSnapshot());
  });

  app.post("/api/notes/characters", (req, res) => {
    try {
      const row = upsertCharacter({
        slug: uniqueCharacterSlug(req.body.display_name),
        display_name: req.body.display_name,
        approval_status: "unapproved",
        release_bucket: req.body.release_bucket || "unassigned",
        unlock_notes: req.body.unlock_notes || "",
        tags: req.body.tags || "",
        roster_group: req.body.roster_group || "",
        source_type: "manual",
        review_status: "approved",
        is_public: 0,
        status: req.body.status || "concept",
      });
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/notes/entries", (req, res) => {
    try {
      const row = upsertMovesetEntry(req.body);
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/notes/media", movesetUpload.single("file"), (req, res) => {
    if (!req.file) {
      res.status(400).json({ ok: false, error: "Move preview file is required." });
      return;
    }

    try {
      const relativeUploadPath = path.relative(DATA_ROOT, req.file.path);
      const asset = createMovesetMedia({
        character_slug: req.body.character_slug,
        section_key: req.body.section_key,
        label: req.body.label,
        original_filename: req.file.originalname,
        stored_filename: req.file.filename,
        relative_upload_path: relativeUploadPath,
        mime_type: req.file.mimetype,
      });
      res.json({ ok: true, asset: movesetMediaWithUrl(asset) });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/notes/media/:id", (req, res) => {
    const asset = getMovesetMediaById(req.params.id);
    if (!asset) {
      res.status(404).json({ ok: false, error: "Move preview not found." });
      return;
    }
    if (asset.relative_upload_path) {
      const filePath = path.join(DATA_ROOT, asset.relative_upload_path);
      if (fs.existsSync(filePath)) {
        fs.rmSync(filePath, { force: true });
      }
    }
    deleteMovesetMedia(req.params.id);
    res.json({ ok: true });
  });

  app.post("/api/admin/characters", (req, res) => {
    try {
      const row = upsertCharacter(req.body);
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/admin/characters/:slug", (req, res) => {
    const slug = req.params.slug;
    for (const asset of listMovesetMedia().filter((row) => row.character_slug === slug)) {
      removeFileIfExists(path.join(DATA_ROOT, asset.relative_upload_path));
    }
    for (const asset of getAdminSnapshot().assets.filter((row) => row.entity_type === "character" && row.entity_slug === slug)) {
      removeFileIfExists(path.join(DATA_ROOT, asset.relative_upload_path));
    }
    deleteCharacter(req.params.slug);
    res.json({ ok: true });
  });

  app.post("/api/notes/characters/:slug", (req, res) => {
    try {
      const row = updateCharacterReview(req.params.slug, {
        display_name: req.body.display_name,
        approval_status: req.body.approval_status,
        release_bucket: req.body.release_bucket,
        unlock_notes: req.body.unlock_notes,
        tags: req.body.tags,
      });
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/notes/characters/:slug", (req, res) => {
    const slug = req.params.slug;
    for (const asset of listMovesetMedia().filter((row) => row.character_slug === slug)) {
      removeFileIfExists(path.join(DATA_ROOT, asset.relative_upload_path));
    }
    for (const asset of getAdminSnapshot().assets.filter((row) => row.entity_type === "character" && row.entity_slug === slug)) {
      removeFileIfExists(path.join(DATA_ROOT, asset.relative_upload_path));
    }
    deleteCharacter(slug);
    res.json({ ok: true, slug });
  });

  app.post("/api/admin/characters/:slug/review", (req, res) => {
    try {
      const row = updateCharacterReview(req.params.slug, req.body);
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/stages", (req, res) => {
    try {
      const row = upsertStage(req.body);
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/admin/stages/:slug", (req, res) => {
    deleteStage(req.params.slug);
    res.json({ ok: true });
  });

  app.post("/api/admin/mechanics", (req, res) => {
    try {
      const row = upsertMechanic(req.body);
      res.json({ ok: true, row });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/admin/mechanics/:slug", (req, res) => {
    deleteMechanic(req.params.slug);
    res.json({ ok: true });
  });

  app.post("/api/admin/assets", adminUpload.single("file"), (req, res) => {
    if (!req.file) {
      res.status(400).json({ ok: false, error: "Asset file is required." });
      return;
    }

    try {
      const relativeUploadPath = path.relative(DATA_ROOT, req.file.path);
      const asset = createAsset({
        entity_type: req.body.entity_type,
        entity_slug: req.body.entity_slug,
        slot: req.body.slot,
        label: req.body.label,
        original_filename: req.file.originalname,
        stored_filename: req.file.filename,
        relative_upload_path: relativeUploadPath,
        mime_type: req.file.mimetype,
        source_type: "admin",
        is_public: toBool(req.body.is_public),
        is_approved: toBool(req.body.is_approved),
        notes: req.body.notes,
      });
      res.json({ ok: true, asset: assetWithUrl(asset) });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.delete("/api/admin/assets/:id", (req, res) => {
    const asset = getAssetById(req.params.id);
    if (asset?.relative_upload_path) {
      const fullPath = path.join(DATA_ROOT, asset.relative_upload_path);
      if (fs.existsSync(fullPath)) {
        fs.rmSync(fullPath, { force: true });
      }
    }
    deleteAsset(req.params.id);
    res.json({ ok: true });
  });

  app.post("/api/admin/assets/:id/review", (req, res) => {
    try {
      const asset = updateAssetReview(req.params.id, req.body);
      res.json({ ok: true, asset: assetWithUrl(asset) });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/submissions/:id", (req, res) => {
    const row = updateSubmission(req.params.id, req.body);
    res.json({ ok: true, row: submissionWithUrl(row) });
  });

  app.delete("/api/admin/submissions/:id", (req, res) => {
    const submission = getSubmissionById(req.params.id);
    if (submission?.file_relative_path) {
      const filePath = path.join(DATA_ROOT, submission.file_relative_path);
      if (fs.existsSync(filePath)) {
        fs.rmSync(filePath, { force: true });
      }
    }
    deleteSubmission(req.params.id);
    res.json({ ok: true });
  });

  app.post("/api/admin/apply", (_req, res) => {
    try {
      const result = runApplyToGodot();
      res.json({ ok: true, ...result });
    } catch (error) {
      res.status(500).json({ ok: false, error: error.message });
    }
  });

  app.get("/api/public/catalog", (_req, res) => {
    res.json(publicCatalog());
  });

  app.post("/api/public/submissions", submissionUpload.single("file"), (req, res) => {
    try {
      const filePayload = req.file
        ? {
            file_relative_path: path.relative(DATA_ROOT, req.file.path),
            file_original_name: req.file.originalname,
            file_mime_type: req.file.mimetype,
          }
        : {};

      const row = createSubmission({
        submitter_name: req.body.submitter_name,
        submitter_contact: req.body.submitter_contact,
        entity_type: req.body.entity_type,
        entity_slug: req.body.entity_slug,
        slot: req.body.slot,
        title: req.body.title,
        description: req.body.description,
        link_url: req.body.link_url,
        status: "pending",
        ...filePayload,
      });

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

  app.get("/api/files/assets/:id", (req, res) => {
    const asset = getAssetById(req.params.id);
    if (!asset) {
      res.status(404).json({ ok: false, error: "Asset not found." });
      return;
    }
    const filePath = path.join(DATA_ROOT, asset.relative_upload_path);
    if (!fs.existsSync(filePath)) {
      res.status(404).json({ ok: false, error: "Asset file missing." });
      return;
    }
    res.sendFile(filePath);
  });

  app.get("/api/public/assets/:id", (req, res) => {
    const asset = getPublicAssetById(req.params.id);
    if (!asset) {
      res.status(404).json({ ok: false, error: "Public asset not found." });
      return;
    }
    const filePath = path.join(DATA_ROOT, asset.relative_upload_path);
    if (!fs.existsSync(filePath)) {
      res.status(404).json({ ok: false, error: "Asset file missing." });
      return;
    }
    res.sendFile(filePath);
  });

  app.get("/api/files/submissions/:id", (req, res) => {
    const submission = getSubmissionById(req.params.id);
    if (!submission?.file_relative_path) {
      res.status(404).json({ ok: false, error: "Submission file not found." });
      return;
    }
    const filePath = path.join(DATA_ROOT, submission.file_relative_path);
    if (!fs.existsSync(filePath)) {
      res.status(404).json({ ok: false, error: "Submission file missing." });
      return;
    }
    res.sendFile(filePath);
  });

  app.get("/api/files/moveset-media/:id", (req, res) => {
    const asset = getMovesetMediaById(req.params.id);
    if (!asset) {
      res.status(404).json({ ok: false, error: "Move preview not found." });
      return;
    }
    const filePath = path.join(DATA_ROOT, asset.relative_upload_path);
    if (!fs.existsSync(filePath)) {
      res.status(404).json({ ok: false, error: "Move preview file missing." });
      return;
    }
    res.sendFile(filePath);
  });

  app.use((error, _req, res, _next) => {
    res.status(500).json({ ok: false, error: error.message });
  });

  return app;
}

export function startServer({ port = DEFAULT_PORT } = {}) {
  const app = createServerApp();
  return new Promise((resolve, reject) => {
    const server = app.listen(port, () => {
      const address = server.address();
      const actualPort = typeof address === "object" && address ? address.port : port;
      resolve({ app, server, port: actualPort });
    });
    server.on("error", reject);
  });
}

const executedPath = process.argv[1] ? fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) : false;

if (executedPath) {
  startServer({ port: DEFAULT_PORT })
    .then(({ port }) => {
      console.log(`Anime Fighter Pro Organizer listening on http://localhost:${port}`);
    })
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });
}
