"use strict";

/**
 * TikSave - High-Performance TikTok Video Downloader
 * Backend: Express.js + TikWM API
 * Compatible with cPanel Node.js Selector (Phusion Passenger)
 */

const express = require("express");
const axios = require("axios");
const cors = require("cors");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const path = require("path");

const app = express();
const PORT = process.env.PORT || 3000;

// ─── Security & Middleware ────────────────────────────────────────────────────

app.use(
  helmet({
    contentSecurityPolicy: false, // We serve inline scripts in index.html
    crossOriginEmbedderPolicy: false,
  })
);

app.use(cors());
app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: true, limit: "1mb" }));

// Serve static files from public directory
app.use(express.static(path.join(__dirname, "public")));

// ─── Rate Limiting ────────────────────────────────────────────────────────────

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 60,                   // max 60 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: {
    success: false,
    error: "Too many requests. Please wait a few minutes and try again.",
  },
});

const proxyLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 30,
  standardHeaders: true,
  legacyHeaders: false,
  message: "Too many download requests. Please slow down.",
});

// ─── Helpers ──────────────────────────────────────────────────────────────────

/**
 * Validates that a string looks like a TikTok URL.
 */
function isValidTikTokUrl(url) {
  if (!url || typeof url !== "string") return false;
  const trimmed = url.trim();
  return /^https?:\/\/(www\.|vm\.|vt\.|m\.)?tiktok\.com\//i.test(trimmed);
}

/**
 * Sanitize a string for use as a filename.
 */
function sanitizeFilename(name = "") {
  return (
    name
      .replace(/[^a-zA-Z0-9\s\-_]/g, "")
      .trim()
      .replace(/\s+/g, "_")
      .substring(0, 80) || "tiksave_video"
  );
}

/**
 * Fetches video data from TikWM public API.
 * Docs: https://www.tikwm.com/
 */
async function fetchFromTikWM(tiktokUrl) {
  const apiUrl = "https://www.tikwm.com/api/";

  const params = new URLSearchParams();
  params.append("url", tiktokUrl);
  params.append("hd", "1"); // Request HD version

  const response = await axios.post(apiUrl, params.toString(), {
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
      Referer: "https://www.tikwm.com/",
    },
    timeout: 20000, // 20s timeout
  });

  return response.data;
}

// ─── Routes ───────────────────────────────────────────────────────────────────

/**
 * GET / — Serve the main page
 */
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

/**
 * POST /api/analyze
 * Body: { url: "https://www.tiktok.com/..." }
 * Returns: metadata + download links
 */
app.post("/api/analyze", apiLimiter, async (req, res) => {
  try {
    const { url } = req.body;

    if (!url) {
      return res.status(400).json({
        success: false,
        error: "Please provide a TikTok URL.",
      });
    }

    if (!isValidTikTokUrl(url)) {
      return res.status(400).json({
        success: false,
        error: "Invalid TikTok URL. Please paste a valid link from TikTok.",
      });
    }

    const tikwmData = await fetchFromTikWM(url.trim());

    // TikWM returns { code: 0, msg: "success", data: {...} } on success
    if (!tikwmData || tikwmData.code !== 0) {
      const errMsg =
        tikwmData?.msg || "Could not fetch video. The link may be private or invalid.";
      return res.status(422).json({ success: false, error: errMsg });
    }

    const d = tikwmData.data;

    // Build a sanitized filename base
    const baseFilename = sanitizeFilename(d.title || "tiksave_video");

    // Build proxy URLs for streaming download through our server
    const buildProxy = (remoteUrl, ext) => {
      if (!remoteUrl) return null;
      const encoded = encodeURIComponent(remoteUrl);
      return `/api/proxy?url=${encoded}&filename=${baseFilename}.${ext}`;
    };

    const result = {
      success: true,
      data: {
        id: d.id || "",
        title: d.title || "TikTok Video",
        author: {
          name: d.author?.nickname || d.author?.unique_id || "Unknown",
          username: d.author?.unique_id || "",
          avatar: d.author?.avatar || "",
        },
        cover: d.cover || d.origin_cover || "",
        duration: d.duration || 0,
        downloads: {
          no_watermark: buildProxy(d.play, "mp4"),
          hd: buildProxy(d.hdplay || d.play, "mp4"),
          mp3: buildProxy(d.music, "mp3"),
          // Direct URLs (may not work in browser due to CORS, but useful as fallback)
          _raw: {
            no_watermark: d.play || null,
            hd: d.hdplay || d.play || null,
            mp3: d.music || null,
          },
        },
      },
    };

    return res.json(result);
  } catch (err) {
    console.error("[/api/analyze] Error:", err.message);

    if (err.code === "ECONNABORTED" || err.message?.includes("timeout")) {
      return res.status(504).json({
        success: false,
        error: "Request timed out. TikWM API is slow right now. Please try again.",
      });
    }

    if (err.response?.status === 429) {
      return res.status(429).json({
        success: false,
        error: "External API rate limit hit. Please wait a moment and retry.",
      });
    }

    return res.status(500).json({
      success: false,
      error: "An unexpected error occurred. Please try again.",
    });
  }
});

/**
 * GET /api/proxy
 * Query params: url (encoded remote URL), filename (desired filename)
 * Streams the remote media file to the client, forcing download.
 */
app.get("/api/proxy", proxyLimiter, async (req, res) => {
  try {
    const { url: rawUrl, filename } = req.query;

    if (!rawUrl) {
      return res.status(400).send("Missing 'url' parameter.");
    }

    // Decode and basic-validate the target URL
    const targetUrl = decodeURIComponent(rawUrl);
    if (!/^https?:\/\//i.test(targetUrl)) {
      return res.status(400).send("Invalid target URL.");
    }

    // Only allow proxying from known safe CDN domains
    const allowedHosts = [
      "tikwm.com",
      "tiktokcdn.com",
      "tiktokv.com",
      "musical.ly",
      "muscdn.com",
      "akamaized.net",
      "tiktok.com",
    ];
    let targetHost;
    try {
      targetHost = new URL(targetUrl).hostname.replace(/^www\./, "");
    } catch {
      return res.status(400).send("Malformed URL.");
    }

    const isAllowed = allowedHosts.some(
      (h) => targetHost === h || targetHost.endsWith(`.${h}`)
    );
    if (!isAllowed) {
      return res.status(403).send("Proxy not allowed for this domain.");
    }

    // Sanitize filename
    const safeFilename = sanitizeFilename(
      filename?.replace(/\.[^/.]+$/, "") || "tiksave_video"
    );
    const ext = (filename?.match(/\.[^/.]+$/) || [".mp4"])[0].replace(
      /[^a-z0-9.]/gi,
      ""
    );
    const finalFilename = `${safeFilename}${ext}`;

    // Determine content-type
    const contentTypeMap = {
      ".mp4": "video/mp4",
      ".mp3": "audio/mpeg",
      ".m4a": "audio/mp4",
      ".webm": "video/webm",
    };
    const contentType = contentTypeMap[ext.toLowerCase()] || "application/octet-stream";

    // Stream the file from the remote CDN
    const upstream = await axios.get(targetUrl, {
      responseType: "stream",
      timeout: 60000,
      headers: {
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        Referer: "https://www.tiktok.com/",
        Accept: "*/*",
        "Accept-Encoding": "identity", // Avoid double-compression
      },
      maxRedirects: 5,
    });

    // Forward content-length if available (lets browser show progress)
    if (upstream.headers["content-length"]) {
      res.setHeader("Content-Length", upstream.headers["content-length"]);
    }

    res.setHeader("Content-Type", contentType);
    res.setHeader(
      "Content-Disposition",
      `attachment; filename="${finalFilename}"`
    );
    res.setHeader("Cache-Control", "no-store");
    res.setHeader("X-Content-Type-Options", "nosniff");

    // Pipe the stream — this is memory-efficient even for large files
    upstream.data.pipe(res);

    upstream.data.on("error", (streamErr) => {
      console.error("[/api/proxy] Stream error:", streamErr.message);
      if (!res.headersSent) {
        res.status(500).send("Stream error.");
      } else {
        res.end();
      }
    });
  } catch (err) {
    console.error("[/api/proxy] Error:", err.message);
    if (!res.headersSent) {
      if (err.response?.status === 403 || err.response?.status === 404) {
        return res
          .status(410)
          .send("Media file is no longer available. Try fetching a fresh link.");
      }
      return res.status(500).send("Failed to proxy media.");
    }
  }
});

/**
 * 404 handler
 */
app.use((req, res) => {
  res.status(404).json({ success: false, error: "Route not found." });
});

/**
 * Global error handler
 */
app.use((err, req, res, _next) => {
  console.error("[Global Error]", err);
  res.status(500).json({ success: false, error: "Internal server error." });
});

// ─── Start Server ─────────────────────────────────────────────────────────────

app.listen(PORT, () => {
  console.log(`✅ TikSave server running on port ${PORT}`);
  console.log(`   → http://localhost:${PORT}`);
});

// Required by Phusion Passenger / cPanel Node.js Selector
module.exports = app;
