const fs = require("fs");
const http = require("http");
const path = require("path");

const { repoRoot } = require("./lib/meals");
const {
  defaultStatePath,
  loadRankingsState,
  recordVote,
  resetRankingsState,
  undoLastRankingsVote,
} = require("./lib/rankings-state");

const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 4321;
const MAX_REQUEST_BODY_BYTES = 16 * 1024;
const MIME_TYPES = {
  ".css": "text/css; charset=utf-8",
  ".gif": "image/gif",
  ".html": "text/html; charset=utf-8",
  ".ico": "image/x-icon",
  ".jpg": "image/jpeg",
  ".js": "application/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".map": "application/json; charset=utf-8",
  ".mp3": "audio/mpeg",
  ".png": "image/png",
  ".svg": "image/svg+xml",
  ".ttf": "font/ttf",
  ".txt": "text/plain; charset=utf-8",
  ".webmanifest": "application/manifest+json; charset=utf-8",
  ".woff": "font/woff",
  ".woff2": "font/woff2",
};

function parseArgs(argv) {
  const options = {
    host: process.env.HOST || DEFAULT_HOST,
    port: Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10),
    rankingsStatePath: path.resolve(process.env.RANKINGS_STATE_PATH || defaultStatePath),
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    const value = argv[index + 1];

    if (arg === "--host" && value) {
      options.host = value;
      index += 1;
      continue;
    }

    if (arg === "--port" && value) {
      options.port = Number.parseInt(value, 10);
      index += 1;
      continue;
    }

    if (arg === "--rankings-state-path" && value) {
      options.rankingsStatePath = path.resolve(value);
      index += 1;
    }
  }

  if (!Number.isInteger(options.port) || options.port <= 0 || options.port > 65535) {
    throw new Error("Port must be an integer between 1 and 65535");
  }

  return options;
}

function getContentType(filePath) {
  return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}

function hasHiddenSegment(pathname) {
  return pathname.split("/").some((segment) => segment.startsWith(".") && segment.length > 1);
}

function resolveRequestPath(pathname) {
  if (hasHiddenSegment(pathname)) {
    return null;
  }

  const requestedPath = pathname === "/" ? "/index.html" : pathname;
  const absolutePath = path.resolve(repoRoot, `.${requestedPath}`);
  const withinRepo =
    absolutePath === repoRoot || absolutePath.startsWith(`${repoRoot}${path.sep}`);

  if (!withinRepo) {
    return null;
  }

  if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
    return path.join(absolutePath, "index.html");
  }

  if (
    path.extname(absolutePath) === "" &&
    !fs.existsSync(absolutePath) &&
    fs.existsSync(`${absolutePath}.html`) &&
    fs.statSync(`${absolutePath}.html`).isFile()
  ) {
    return `${absolutePath}.html`;
  }

  return absolutePath;
}

function sendJson(response, statusCode, payload) {
  response.writeHead(statusCode, {
    "Cache-Control": "no-store",
    "Content-Type": "application/json; charset=utf-8",
  });
  response.end(`${JSON.stringify(payload, null, 2)}\n`);
}

function sendText(response, statusCode, body) {
  response.writeHead(statusCode, {
    "Cache-Control": "no-store",
    "Content-Type": "text/plain; charset=utf-8",
  });
  response.end(body);
}

function sendRedirect(response, location) {
  response.writeHead(301, {
    Location: location,
    "Cache-Control": "no-store",
  });
  response.end();
}

function parseJsonBody(request) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    let totalBytes = 0;

    request.on("data", (chunk) => {
      totalBytes += chunk.length;

      if (totalBytes > MAX_REQUEST_BODY_BYTES) {
        reject(new Error("Request body is too large"));
        request.destroy();
        return;
      }

      chunks.push(chunk);
    });

    request.on("end", () => {
      if (chunks.length === 0) {
        resolve({});
        return;
      }

      try {
        resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
      } catch (error) {
        reject(new Error("Request body must be valid JSON"));
      }
    });

    request.on("error", reject);
  });
}

function sendFile(response, filePath) {
  const stream = fs.createReadStream(filePath);

  response.writeHead(200, {
    "Content-Type": getContentType(filePath),
    "Cache-Control": "no-cache",
  });

  stream.pipe(response);
  stream.on("error", () => {
    sendText(response, 500, "Failed to read file");
  });
}

async function handleApiRequest(request, response, pathname, options) {
  if (pathname === "/api/rankings" && request.method === "GET") {
    sendJson(response, 200, loadRankingsState({ statePath: options.rankingsStatePath }));
    return true;
  }

  if (pathname === "/api/rankings/vote" && request.method === "POST") {
    const body = await parseJsonBody(request);
    sendJson(response, 200, recordVote(body, { statePath: options.rankingsStatePath }));
    return true;
  }

  if (pathname === "/api/rankings/reset" && request.method === "POST") {
    sendJson(response, 200, resetRankingsState({ statePath: options.rankingsStatePath }));
    return true;
  }

  if (pathname === "/api/rankings/undo" && request.method === "POST") {
    sendJson(response, 200, undoLastRankingsVote({ statePath: options.rankingsStatePath }));
    return true;
  }

  if (
    pathname === "/api/rankings" ||
    pathname === "/api/rankings/vote" ||
    pathname === "/api/rankings/reset" ||
    pathname === "/api/rankings/undo"
  ) {
    sendText(response, 405, "Method not allowed");
    return true;
  }

  if (pathname.startsWith("/api/")) {
    sendText(response, 404, "Not found");
    return true;
  }

  return false;
}

function createServer(options) {
  return http.createServer(async (request, response) => {
    let pathname;
    let search;

    try {
      const requestUrl = new URL(request.url || "/", "http://localhost");
      pathname = decodeURIComponent(requestUrl.pathname);
      search = requestUrl.search;
    } catch (error) {
      sendText(response, 400, "Bad request");
      return;
    }

    if (request.method === "GET" || request.method === "HEAD") {
      if (pathname === "/index.html") {
        sendRedirect(response, `/${search}`);
        return;
      }

      if (pathname.endsWith(".html") && pathname !== "/index.html") {
        sendRedirect(response, `${pathname.slice(0, -5)}${search}`);
        return;
      }
    }

    try {
      if (await handleApiRequest(request, response, pathname, options)) {
        return;
      }
    } catch (error) {
      const statusCode =
        error.message === "Request body must be valid JSON" ||
        error.message === "Request body is too large" ||
        error.message.startsWith("Vote payload") ||
        error.message === "winnerId and loserId must be different"
          ? 400
          : 500;

      if (statusCode >= 500) {
        console.error(error);
      }

      sendJson(response, statusCode, { error: error.message });
      return;
    }

    const filePath = resolveRequestPath(pathname);

    if (!filePath) {
      sendText(response, 403, "Forbidden");
      return;
    }

    if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
      sendText(response, 404, "Not found");
      return;
    }

    sendFile(response, filePath);
  });
}

function main() {
  const options = parseArgs(process.argv.slice(2));
  const server = createServer(options);

  server.listen(options.port, options.host, () => {
    console.log(`Serving ${repoRoot} at http://${options.host}:${options.port}`);
    console.log(`Rankings state path: ${options.rankingsStatePath}`);
  });
}

if (require.main === module) {
  try {
    main();
  } catch (error) {
    console.error(error.message);
    process.exitCode = 1;
  }
}
