import { createServer } from "node:http";
import { mkdirSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const dataDir = join(__dirname, "data");
mkdirSync(dataDir, { recursive: true });

const databasePath = join(dataDir, "tamilcanada-dating.sqlite");
const db = new DatabaseSync(databasePath);
const port = Number(process.env.PORT ?? 8080);
const host = process.env.HOST ?? "0.0.0.0";
const allowedOrigins = new Set(
  (process.env.ALLOWED_ORIGINS ??
    "https://tamilcanada.com,https://www.tamilcanada.com,http://127.0.0.1:3000,http://localhost:3000,http://127.0.0.1:8080,http://localhost:8080")
    .split(",")
    .map((origin) => origin.trim())
    .filter(Boolean),
);
const demoPassword = process.env.DEMO_USER_PASSWORD ?? "TamilLove123!";
const defaultAdminEmail = process.env.ADMIN_EMAIL ?? "admin@tamilcanada.com";
const defaultAdminPassword = process.env.ADMIN_PASSWORD ?? "ChangeMeNow123!";

db.exec(`
  PRAGMA journal_mode = WAL;

  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    age INTEGER NOT NULL,
    bio TEXT NOT NULL,
    city TEXT NOT NULL,
    occupation TEXT NOT NULL,
    gender TEXT NOT NULL,
    interested_in TEXT NOT NULL,
    prompt TEXT NOT NULL,
    interests_json TEXT NOT NULL,
    photos_json TEXT NOT NULL,
    distance_km INTEGER NOT NULL DEFAULT 5,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  );

  CREATE TABLE IF NOT EXISTS sessions (
    token TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
  );

  CREATE TABLE IF NOT EXISTS swipes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    from_user_id INTEGER NOT NULL,
    to_user_id INTEGER NOT NULL,
    direction TEXT NOT NULL CHECK(direction IN ('like', 'pass')),
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(from_user_id, to_user_id),
    FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE
  );

  CREATE TABLE IF NOT EXISTS matches (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_one_id INTEGER NOT NULL,
    user_two_id INTEGER NOT NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_one_id, user_two_id),
    FOREIGN KEY (user_one_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (user_two_id) REFERENCES users(id) ON DELETE CASCADE
  );

  CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    match_id INTEGER NOT NULL,
    sender_id INTEGER NOT NULL,
    text TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
    FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE
  );

  CREATE TABLE IF NOT EXISTS admin_users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    password_salt TEXT NOT NULL,
    password_hash TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  );

  CREATE TABLE IF NOT EXISTS admin_sessions (
    token TEXT PRIMARY KEY,
    admin_id INTEGER NOT NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (admin_id) REFERENCES admin_users(id) ON DELETE CASCADE
  );
`);

ensureUserAuthColumns();
seedDemoUsers();
seedDefaultAdmin();

const server = createServer(async (request, response) => {
  try {
    setCors(request, response);

    if (request.method === "OPTIONS") {
      response.writeHead(204);
      response.end();
      return;
    }

    const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);

    if (request.method === "GET" && url.pathname === "/api/health") {
      return sendJson(response, 200, {
        ok: true,
        service: "tamilcanada-dating-api",
        domain: "tamilcanada.com",
        databasePath,
      });
    }

    if (request.method === "POST" && url.pathname === "/api/auth/signup") {
      const body = await readJsonBody(request);
      const session = signUp(body);
      return sendJson(response, 201, session);
    }

    if (request.method === "POST" && url.pathname === "/api/auth/login") {
      const body = await readJsonBody(request);
      const session = login(body);
      return sendJson(response, 200, session);
    }

    if (request.method === "POST" && url.pathname === "/api/admin/login") {
      const body = await readJsonBody(request);
      const session = loginAdmin(body);
      return sendJson(response, 200, session);
    }

    if (request.method === "GET" && url.pathname === "/api/admin/me") {
      const admin = requireAdminAuth(request);
      return sendJson(response, 200, { admin });
    }

    if (request.method === "GET" && url.pathname === "/api/admin/stats") {
      requireAdminAuth(request);
      return sendJson(response, 200, getAdminStats());
    }

    if (request.method === "GET" && url.pathname === "/api/admin/users") {
      requireAdminAuth(request);
      return sendJson(response, 200, {
        users: getAdminUsers(),
      });
    }

    const user = requireAuth(request);

    if (request.method === "GET" && url.pathname === "/api/me") {
      return sendJson(response, 200, { user });
    }

    if (request.method === "PUT" && url.pathname === "/api/me") {
      const body = await readJsonBody(request);
      const updatedUser = updateProfile(user.id, body);
      return sendJson(response, 200, { user: updatedUser });
    }

    if (request.method === "GET" && url.pathname === "/api/discover") {
      return sendJson(response, 200, {
        profiles: getDiscoverProfiles(user.id),
      });
    }

    if (request.method === "POST" && url.pathname === "/api/swipes") {
      const body = await readJsonBody(request);
      const result = createSwipe(user.id, body);
      return sendJson(response, 200, result);
    }

    if (request.method === "GET" && url.pathname === "/api/matches") {
      return sendJson(response, 200, {
        matches: getMatches(user.id),
      });
    }

    if (request.method === "GET" && url.pathname === "/api/chats") {
      return sendJson(response, 200, {
        chats: getChats(user.id),
      });
    }

    const messagesMatch = url.pathname.match(/^\/api\/matches\/(\d+)\/messages$/);
    if (messagesMatch && request.method === "GET") {
      const matchId = Number(messagesMatch[1]);
      return sendJson(response, 200, {
        messages: getMessages(user.id, matchId),
      });
    }

    if (messagesMatch && request.method === "POST") {
      const matchId = Number(messagesMatch[1]);
      const body = await readJsonBody(request);
      const message = sendMessage(user.id, matchId, body);
      return sendJson(response, 201, { message });
    }

    return sendJson(response, 404, {
      error: "Route not found",
    });
  } catch (error) {
    const statusCode = error.statusCode ?? 500;
    return sendJson(response, statusCode, {
      error: error.message ?? "Unexpected server error",
    });
  }
});

if (process.env.HEARTMATCH_SKIP_LISTEN !== "1") {
  server.listen(port, host, () => {
    console.log(`TamilCanada dating API listening on http://${host}:${port}`);
  });
}

function ensureUserAuthColumns() {
  const userColumns = getColumnNames("users");

  if (!userColumns.has("password_salt")) {
    db.exec("ALTER TABLE users ADD COLUMN password_salt TEXT");
  }
  if (!userColumns.has("password_hash")) {
    db.exec("ALTER TABLE users ADD COLUMN password_hash TEXT");
  }
}

function seedDemoUsers() {
  const seedUsers = [
    {
      name: "Mia",
      email: "mia@example.com",
      age: 24,
      bio: "Coffee first, beach sunset later. Looking for something real with a little chaos.",
      city: "Toronto",
      occupation: "Product Designer",
      gender: "Woman",
      interestedIn: "Men",
      prompt: "A perfect Sunday is vintage shopping, ramen, and disappearing into a bookstore.",
      interests: ["Travel", "Pilates", "Ramen"],
      distanceKm: 3,
    },
    {
      name: "Noah",
      email: "noah@example.com",
      age: 27,
      bio: "Startup builder by day, amateur pasta scientist by night.",
      city: "Toronto",
      occupation: "Founder",
      gender: "Man",
      interestedIn: "Women",
      prompt: "Teach me your best comfort-food recipe and I am already interested.",
      interests: ["Cooking", "Running", "AI"],
      distanceKm: 6,
    },
    {
      name: "Ava",
      email: "ava@example.com",
      age: 25,
      bio: "Always planning the next concert, hike, or spontaneous road trip.",
      city: "Mississauga",
      occupation: "Event Manager",
      gender: "Woman",
      interestedIn: "Men",
      prompt: "I will absolutely make you a playlist if we vibe.",
      interests: ["Music", "Road Trips", "Hiking"],
      distanceKm: 9,
    },
    {
      name: "Ethan",
      email: "ethan@example.com",
      age: 29,
      bio: "I lift weights, read fiction, and overthink smoothie ingredients.",
      city: "North York",
      occupation: "Physiotherapist",
      gender: "Man",
      interestedIn: "Women",
      prompt: "Green flags are kindness, ambition, and being nice to service staff.",
      interests: ["Fitness", "Books", "Smoothies"],
      distanceKm: 4,
    },
    {
      name: "Sophia",
      email: "sophia@example.com",
      age: 26,
      bio: "Marketing lead, amateur baker, and aggressively competitive at board games.",
      city: "Toronto",
      occupation: "Marketing Lead",
      gender: "Woman",
      interestedIn: "Men",
      prompt: "Best first date: walk, dessert, and a little playful roasting.",
      interests: ["Baking", "Board Games", "Fashion"],
      distanceKm: 7,
    },
    {
      name: "Lucas",
      email: "lucas@example.com",
      age: 28,
      bio: "Film camera collector. I know a hidden patio for every mood.",
      city: "Etobicoke",
      occupation: "Architect",
      gender: "Man",
      interestedIn: "Women",
      prompt: "Convince me your neighborhood has the best late-night food.",
      interests: ["Architecture", "Photography", "Food"],
      distanceKm: 11,
    },
  ];

  const insertStatement = db.prepare(`
    INSERT INTO users (
      name, email, age, bio, city, occupation, gender, interested_in, prompt,
      interests_json, photos_json, distance_km, password_salt, password_hash
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);

  const updatePasswordStatement = db.prepare(`
    UPDATE users
    SET password_salt = ?, password_hash = ?
    WHERE id = ?
  `);

  for (const [index, user] of seedUsers.entries()) {
    const auth = createPasswordHash(demoPassword);
    const existing = db.prepare("SELECT id, password_hash FROM users WHERE email = ?").get(user.email);

    if (!existing) {
      insertStatement.run(
        user.name,
        user.email,
        user.age,
        user.bio,
        user.city,
        user.occupation,
        user.gender,
        user.interestedIn,
        user.prompt,
        JSON.stringify(user.interests),
        JSON.stringify([`https://picsum.photos/seed/tamilcanada-${index + 1}/800/1200`]),
        user.distanceKm,
        auth.salt,
        auth.hash,
      );
      continue;
    }

    if (!existing.password_hash) {
      updatePasswordStatement.run(auth.salt, auth.hash, existing.id);
    }
  }
}

function seedDefaultAdmin() {
  const existing = db.prepare("SELECT id FROM admin_users WHERE email = ?").get(defaultAdminEmail);
  if (existing) {
    return;
  }

  const auth = createPasswordHash(defaultAdminPassword);
  db.prepare(`
    INSERT INTO admin_users (name, email, password_salt, password_hash)
    VALUES (?, ?, ?, ?)
  `).run("TamilCanada Admin", defaultAdminEmail, auth.salt, auth.hash);
}

function signUp(body) {
  const name = requiredString(body.name, "name");
  const email = normalizeEmail(requiredString(body.email, "email"));
  const password = validatePassword(body.password);
  const age = Number(body.age ?? 24);
  const city = requiredString(body.city, "city");
  const bio = requiredString(body.bio, "bio");
  const occupation = requiredString(body.occupation, "occupation");
  const gender = requiredString(body.gender, "gender");
  const interestedIn = requiredString(body.interestedIn, "interestedIn");
  const prompt = requiredString(body.prompt, "prompt");
  const interests = Array.isArray(body.interests) ? body.interests.filter(Boolean) : [];

  const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
  if (existing) {
    throw createHttpError(409, "An account with this email already exists");
  }

  const auth = createPasswordHash(password);
  const result = db.prepare(`
    INSERT INTO users (
      name, email, age, bio, city, occupation, gender, interested_in,
      prompt, interests_json, photos_json, distance_km, password_salt, password_hash
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    name,
    email,
    age,
    bio,
    city,
    occupation,
    gender,
    interestedIn,
    prompt,
    JSON.stringify(interests),
    JSON.stringify([]),
    Math.max(1, Math.floor(Math.random() * 15)),
    auth.salt,
    auth.hash,
  );

  return createSessionResponse(Number(result.lastInsertRowid));
}

function login(body) {
  const email = normalizeEmail(requiredString(body.email, "email"));
  const password = validatePassword(body.password);
  const user = db.prepare("SELECT id, password_salt, password_hash FROM users WHERE email = ?").get(email);

  if (!user || !user.password_salt || !user.password_hash) {
    throw createHttpError(401, "Invalid email or password");
  }

  assertPassword(password, user.password_salt, user.password_hash);
  return createSessionResponse(user.id);
}

function loginAdmin(body) {
  const email = normalizeEmail(requiredString(body.email, "email"));
  const password = validatePassword(body.password);
  const admin = db.prepare(`
    SELECT id, name, email, password_salt, password_hash
    FROM admin_users
    WHERE email = ?
  `).get(email);

  if (!admin) {
    throw createHttpError(401, "Invalid admin email or password");
  }

  assertPassword(password, admin.password_salt, admin.password_hash);
  const token = randomUUID();
  db.prepare("INSERT INTO admin_sessions (token, admin_id) VALUES (?, ?)").run(token, admin.id);

  return {
    token,
    admin: {
      id: admin.id,
      name: admin.name,
      email: admin.email,
    },
  };
}

function createSessionResponse(userId) {
  const token = randomUUID();
  db.prepare("INSERT INTO sessions (token, user_id) VALUES (?, ?)").run(token, userId);
  return {
    token,
    user: getUserById(userId),
  };
}

function requireAuth(request) {
  const token = extractBearerToken(request);
  const session = db.prepare("SELECT user_id FROM sessions WHERE token = ?").get(token);
  if (!session) {
    throw createHttpError(401, "Invalid session");
  }

  return getUserById(session.user_id);
}

function requireAdminAuth(request) {
  const token = extractBearerToken(request);
  const session = db.prepare("SELECT admin_id FROM admin_sessions WHERE token = ?").get(token);
  if (!session) {
    throw createHttpError(401, "Invalid admin session");
  }

  const admin = db.prepare("SELECT id, name, email FROM admin_users WHERE id = ?").get(session.admin_id);
  if (!admin) {
    throw createHttpError(401, "Admin account no longer exists");
  }

  return admin;
}

function updateProfile(userId, body) {
  const current = getUserById(userId);
  const next = {
    name: body.name ? requiredString(body.name, "name") : current.name,
    age: body.age ? Number(body.age) : current.age,
    bio: body.bio ? requiredString(body.bio, "bio") : current.bio,
    city: body.city ? requiredString(body.city, "city") : current.city,
    occupation: body.occupation ? requiredString(body.occupation, "occupation") : current.occupation,
    gender: body.gender ? requiredString(body.gender, "gender") : current.gender,
    interestedIn: body.interestedIn ? requiredString(body.interestedIn, "interestedIn") : current.interestedIn,
    prompt: body.prompt ? requiredString(body.prompt, "prompt") : current.prompt,
    interests: Array.isArray(body.interests) ? body.interests.filter(Boolean) : current.interests,
    photos: Array.isArray(body.photos) ? body.photos.filter(Boolean) : current.photos,
  };

  db.prepare(`
    UPDATE users
    SET
      name = ?,
      age = ?,
      bio = ?,
      city = ?,
      occupation = ?,
      gender = ?,
      interested_in = ?,
      prompt = ?,
      interests_json = ?,
      photos_json = ?
    WHERE id = ?
  `).run(
    next.name,
    next.age,
    next.bio,
    next.city,
    next.occupation,
    next.gender,
    next.interestedIn,
    next.prompt,
    JSON.stringify(next.interests),
    JSON.stringify(next.photos),
    userId,
  );

  return getUserById(userId);
}

function getDiscoverProfiles(userId) {
  return db.prepare(`
    SELECT *
    FROM users
    WHERE id != ?
      AND id NOT IN (
        SELECT to_user_id
        FROM swipes
        WHERE from_user_id = ?
      )
    ORDER BY distance_km ASC, id ASC
    LIMIT 20
  `).all(userId, userId).map(mapUserRow);
}

function createSwipe(userId, body) {
  const targetUserId = Number(body.targetUserId);
  const direction = body.direction === "like" ? "like" : body.direction === "pass" ? "pass" : null;

  if (!Number.isInteger(targetUserId)) {
    throw createHttpError(400, "targetUserId must be a number");
  }
  if (!direction) {
    throw createHttpError(400, "direction must be 'like' or 'pass'");
  }
  if (targetUserId === userId) {
    throw createHttpError(400, "You cannot swipe on yourself");
  }

  db.prepare(`
    INSERT INTO swipes (from_user_id, to_user_id, direction)
    VALUES (?, ?, ?)
    ON CONFLICT(from_user_id, to_user_id)
    DO UPDATE SET direction = excluded.direction, created_at = CURRENT_TIMESTAMP
  `).run(userId, targetUserId, direction);

  if (direction !== "like") {
    return { didMatch: false };
  }

  const reciprocal = db.prepare(`
    SELECT id
    FROM swipes
    WHERE from_user_id = ? AND to_user_id = ? AND direction = 'like'
  `).get(targetUserId, userId);

  if (!reciprocal) {
    return { didMatch: false };
  }

  const userOneId = Math.min(userId, targetUserId);
  const userTwoId = Math.max(userId, targetUserId);

  db.prepare(`
    INSERT OR IGNORE INTO matches (user_one_id, user_two_id)
    VALUES (?, ?)
  `).run(userOneId, userTwoId);

  const match = db.prepare(`
    SELECT id, created_at
    FROM matches
    WHERE user_one_id = ? AND user_two_id = ?
  `).get(userOneId, userTwoId);

  return {
    didMatch: true,
    match: {
      id: match.id,
      matchedAt: match.created_at,
      user: getUserById(targetUserId),
      lastMessage: null,
    },
  };
}

function getMatches(userId) {
  const matches = db.prepare(`
    SELECT
      m.id,
      m.created_at,
      CASE
        WHEN m.user_one_id = ? THEN m.user_two_id
        ELSE m.user_one_id
      END AS other_user_id
    FROM matches m
    WHERE m.user_one_id = ? OR m.user_two_id = ?
    ORDER BY m.created_at DESC
  `).all(userId, userId, userId);

  const lastMessageStatement = db.prepare(`
    SELECT text
    FROM messages
    WHERE match_id = ?
    ORDER BY created_at DESC, id DESC
    LIMIT 1
  `);

  return matches.map((match) => ({
    id: match.id,
    matchedAt: match.created_at,
    user: getUserById(match.other_user_id),
    lastMessage: lastMessageStatement.get(match.id)?.text ?? null,
  }));
}

function getChats(userId) {
  const matches = db.prepare(`
    SELECT
      m.id,
      CASE
        WHEN m.user_one_id = ? THEN m.user_two_id
        ELSE m.user_one_id
      END AS other_user_id
    FROM matches m
    WHERE m.user_one_id = ? OR m.user_two_id = ?
    ORDER BY m.created_at DESC
  `).all(userId, userId, userId);

  const lastMessageStatement = db.prepare(`
    SELECT id, sender_id, text, created_at
    FROM messages
    WHERE match_id = ?
    ORDER BY created_at DESC, id DESC
    LIMIT 1
  `);

  return matches.map((match) => {
    const lastMessage = lastMessageStatement.get(match.id);
    return {
      id: match.id,
      matchId: match.id,
      otherUser: getUserById(match.other_user_id),
      lastMessage: lastMessage
        ? {
            id: lastMessage.id,
            matchId: match.id,
            senderId: lastMessage.sender_id,
            text: lastMessage.text,
            createdAt: lastMessage.created_at,
          }
        : null,
    };
  });
}

function getMessages(userId, matchId) {
  const match = getAuthorizedMatch(userId, matchId);
  return db.prepare(`
    SELECT id, sender_id, text, created_at
    FROM messages
    WHERE match_id = ?
    ORDER BY created_at ASC, id ASC
  `).all(match.id).map((row) => ({
    id: row.id,
    matchId,
    senderId: row.sender_id,
    text: row.text,
    createdAt: row.created_at,
  }));
}

function sendMessage(userId, matchId, body) {
  getAuthorizedMatch(userId, matchId);
  const text = requiredString(body.text, "text");
  const result = db.prepare(`
    INSERT INTO messages (match_id, sender_id, text)
    VALUES (?, ?, ?)
  `).run(matchId, userId, text);

  const row = db.prepare(`
    SELECT id, sender_id, text, created_at
    FROM messages
    WHERE id = ?
  `).get(Number(result.lastInsertRowid));

  return {
    id: row.id,
    matchId,
    senderId: row.sender_id,
    text: row.text,
    createdAt: row.created_at,
  };
}

function getAuthorizedMatch(userId, matchId) {
  const match = db.prepare(`
    SELECT id, user_one_id, user_two_id
    FROM matches
    WHERE id = ?
  `).get(matchId);

  if (!match) {
    throw createHttpError(404, "Match not found");
  }
  if (match.user_one_id !== userId && match.user_two_id !== userId) {
    throw createHttpError(403, "You are not part of this match");
  }

  return match;
}

function getUserById(userId) {
  const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
  if (!row) {
    throw createHttpError(404, "User not found");
  }
  return mapUserRow(row);
}

function getAdminStats() {
  return {
    users: scalar("SELECT COUNT(*) AS value FROM users"),
    matches: scalar("SELECT COUNT(*) AS value FROM matches"),
    messages: scalar("SELECT COUNT(*) AS value FROM messages"),
    admins: scalar("SELECT COUNT(*) AS value FROM admin_users"),
  };
}

function getAdminUsers() {
  return db.prepare(`
    SELECT id, name, email, city, age, occupation, created_at
    FROM users
    ORDER BY created_at DESC, id DESC
  `).all().map((row) => ({
    id: row.id,
    name: row.name,
    email: row.email,
    city: row.city,
    age: row.age,
    occupation: row.occupation,
    createdAt: row.created_at,
  }));
}

function mapUserRow(row) {
  return {
    id: row.id,
    name: row.name,
    email: row.email,
    age: row.age,
    bio: row.bio,
    city: row.city,
    occupation: row.occupation,
    gender: row.gender,
    interestedIn: row.interested_in,
    prompt: row.prompt,
    interests: JSON.parse(row.interests_json ?? "[]"),
    photos: JSON.parse(row.photos_json ?? "[]"),
    distanceKm: row.distance_km,
  };
}

function createPasswordHash(password) {
  const salt = randomBytes(16).toString("hex");
  const hash = scryptSync(password, salt, 64).toString("hex");
  return { salt, hash };
}

function assertPassword(password, salt, expectedHash) {
  const actual = Buffer.from(scryptSync(password, salt, 64).toString("hex"), "hex");
  const expected = Buffer.from(expectedHash, "hex");

  if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
    throw createHttpError(401, "Invalid email or password");
  }
}

function validatePassword(value) {
  if (typeof value !== "string" || value.length < 8) {
    throw createHttpError(400, "password must be at least 8 characters");
  }
  return value;
}

function extractBearerToken(request) {
  const header = request.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    throw createHttpError(401, "Missing bearer token");
  }
  return header.slice("Bearer ".length);
}

function getColumnNames(tableName) {
  return new Set(
    db.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => row.name),
  );
}

function scalar(query, ...params) {
  return db.prepare(query).get(...params)?.value ?? 0;
}

function requiredString(value, fieldName) {
  if (typeof value !== "string" || value.trim().length === 0) {
    throw createHttpError(400, `${fieldName} is required`);
  }
  return value.trim();
}

function normalizeEmail(value) {
  return value.trim().toLowerCase();
}

function createHttpError(statusCode, message) {
  const error = new Error(message);
  error.statusCode = statusCode;
  return error;
}

function setCors(request, response) {
  const origin = request.headers.origin;
  if (origin && allowedOrigins.has(origin)) {
    response.setHeader("Access-Control-Allow-Origin", origin);
    response.setHeader("Vary", "Origin");
  } else if (!origin) {
    response.setHeader("Access-Control-Allow-Origin", "*");
  }

  response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
  response.setHeader("Content-Type", "application/json; charset=utf-8");
}

async function readJsonBody(request) {
  let body = "";
  for await (const chunk of request) {
    body += chunk;
  }

  if (!body) {
    return {};
  }

  try {
    return JSON.parse(body);
  } catch {
    throw createHttpError(400, "Invalid JSON body");
  }
}

function sendJson(response, statusCode, payload) {
  response.writeHead(statusCode);
  response.end(JSON.stringify(payload));
}

export {
  createSwipe,
  getAdminStats,
  getAdminUsers,
  getChats,
  getDiscoverProfiles,
  getMatches,
  getMessages,
  getUserById,
  login,
  loginAdmin,
  sendMessage,
  signUp,
  updateProfile,
};
