"use server";

import { revalidatePath } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { getEmbedAllowlist } from "@/lib/embeds";
import {
  createSession,
  SESSION_COOKIE,
  SESSION_MAX_AGE_SECONDS,
  verifySession,
} from "@/lib/session";
import {
  getAdminUsername,
  isAdminConfigured,
  verifyPassword,
} from "@/lib/password";
import {
  slugify,
  sourceDomainOf,
  validateCategoryInput,
  validateGameInput,
  type GameFieldErrors,
} from "@/lib/validation";

/* ---------------- guards ---------------- */

/** Throw a redirect to login when the admin session is missing/invalid. */
export async function requireAdmin(): Promise<string> {
  const token = (await cookies()).get(SESSION_COOKIE)?.value;
  const user = token ? await verifySession(token) : null;
  if (!user) redirect("/admin/login");
  return user;
}

/* ---------------- login rate limiting (in-memory, per instance) ---------------- */

const LOGIN_WINDOW_MS = 10 * 60 * 1000;
const LOGIN_MAX_ATTEMPTS = 5;
const loginAttempts = new Map<string, { count: number; firstAt: number }>();

function loginBlocked(key: string): boolean {
  const entry = loginAttempts.get(key);
  if (!entry) return false;
  if (Date.now() - entry.firstAt > LOGIN_WINDOW_MS) {
    loginAttempts.delete(key);
    return false;
  }
  return entry.count >= LOGIN_MAX_ATTEMPTS;
}

function recordFailedLogin(key: string): void {
  const entry = loginAttempts.get(key);
  if (!entry || Date.now() - entry.firstAt > LOGIN_WINDOW_MS) {
    loginAttempts.set(key, { count: 1, firstAt: Date.now() });
  } else {
    entry.count += 1;
  }
}

async function clientKey(): Promise<string> {
  const h = await headers();
  return (
    h.get("x-forwarded-for")?.split(",")[0]?.trim() ||
    h.get("x-real-ip") ||
    "unknown"
  );
}

/* ---------------- auth actions ---------------- */

export interface LoginState {
  error: string;
}

export async function loginAction(
  _prev: LoginState,
  formData: FormData,
): Promise<LoginState> {
  const key = await clientKey();
  if (loginBlocked(key)) {
    return {
      error: "Too many failed attempts. Try again in a few minutes.",
    };
  }
  if (!isAdminConfigured()) {
    return { error: "Admin access is not configured on this server." };
  }
  const username = String(formData.get("username") ?? "").trim();
  const password = String(formData.get("password") ?? "");
  const next = String(formData.get("next") ?? "/admin");

  const expected = getAdminUsername();
  const ok =
    expected !== null &&
    username === expected &&
    (await verifyPassword(password));
  if (!ok) {
    recordFailedLogin(key);
    // Same message either way — no username enumeration.
    return { error: "Invalid username or password." };
  }
  loginAttempts.delete(key);

  const token = await createSession(username);
  if (!token) {
    return { error: "Could not create a session. Check server configuration." };
  }
  (await cookies()).set(SESSION_COOKIE, token, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    path: "/",
    maxAge: SESSION_MAX_AGE_SECONDS,
  });
  redirect(next.startsWith("/admin") ? next : "/admin");
}

export async function logoutAction(): Promise<void> {
  (await cookies()).delete(SESSION_COOKIE);
  redirect("/admin/login");
}

/* ---------------- game actions ---------------- */

function categoryExistsSync(ids: Set<number>, id: number): boolean {
  return ids.has(id);
}

async function categoryIdSet(): Promise<Set<number>> {
  const cats = await db.category.findMany({ select: { id: true } });
  return new Set(cats.map((c) => c.id));
}

function revalidateGame(slug: string, categorySlug: string): void {
  revalidatePath("/");
  revalidatePath("/games");
  revalidatePath("/categories");
  revalidatePath(`/category/${categorySlug}`);
  revalidatePath(`/games/${slug}`);
  revalidatePath(`/games/${slug}/play`);
  revalidatePath("/favorites");
  revalidatePath("/recently-played");
  revalidatePath("/admin");
  revalidatePath("/admin/games");
}

export interface GameFormState {
  errors: GameFieldErrors;
  values: Record<string, string>;
  message: string;
}

function formValues(formData: FormData): Record<string, string> {
  const values: Record<string, string> = {};
  for (const [k, v] of formData.entries()) {
    if (typeof v === "string") values[k] = v;
  }
  return values;
}

async function uniqueSlug(base: string, ignoreId?: number): Promise<string> {
  let slug = base || "game";
  let n = 0;
  for (;;) {
    const existing = await db.game.findUnique({
      where: { slug },
      select: { id: true },
    });
    if (!existing || existing.id === ignoreId) return slug;
    n += 1;
    slug = `${base}-${n}`;
  }
}

export async function createGameAction(
  _prev: GameFormState,
  formData: FormData,
): Promise<GameFormState> {
  await requireAdmin();
  const raw = formValues(formData);
  const ids = await categoryIdSet();
  const { data, errors } = validateGameInput(raw, {
    categoryExists: (id) => categoryExistsSync(ids, id),
    allowlist: getEmbedAllowlist(),
  });
  if (Object.keys(errors).length > 0) {
    return { errors, values: raw, message: "Fix the highlighted problems and try again." };
  }
  const slug = await uniqueSlug(data.slug || slugify(data.title));
  const created = await db.game.create({
    data: {
      slug,
      title: data.title,
      description: data.description,
      categoryId: data.categoryId,
      developer: data.developer || null,
      publisher: data.publisher || null,
      officialUrl: data.officialUrl,
      sourceDomain: sourceDomainOf(data.officialUrl),
      thumbnail: data.thumbnail || null,
      embedUrl: data.embedUrl || null,
      embedAllowed: data.embedAllowed,
      status: data.status,
      featured: data.featured,
      trending: data.trending,
      instructions: data.instructions,
      controls: data.controls,
    },
    include: { category: true },
  });
  revalidateGame(created.slug, created.category.slug);
  redirect("/admin/games");
}

export async function updateGameAction(
  id: number,
  _prev: GameFormState,
  formData: FormData,
): Promise<GameFormState> {
  await requireAdmin();
  const raw = formValues(formData);
  const ids = await categoryIdSet();
  const { data, errors } = validateGameInput(raw, {
    categoryExists: (id) => categoryExistsSync(ids, id),
    allowlist: getEmbedAllowlist(),
  });
  const existing = await db.game.findUnique({
    where: { id },
    select: { id: true },
  });
  if (!existing) {
    return { errors: {}, values: raw, message: "That game no longer exists." };
  }
  if (Object.keys(errors).length > 0) {
    return { errors, values: raw, message: "Fix the highlighted problems and try again." };
  }
  const slug = await uniqueSlug(data.slug || slugify(data.title), id);
  const updated = await db.game.update({
    where: { id },
    data: {
      slug,
      title: data.title,
      description: data.description,
      categoryId: data.categoryId,
      developer: data.developer || null,
      publisher: data.publisher || null,
      officialUrl: data.officialUrl,
      sourceDomain: sourceDomainOf(data.officialUrl),
      thumbnail: data.thumbnail || null,
      embedUrl: data.embedUrl || null,
      embedAllowed: data.embedAllowed,
      status: data.status,
      featured: data.featured,
      trending: data.trending,
      instructions: data.instructions,
      controls: data.controls,
    },
    include: { category: true },
  });
  revalidateGame(updated.slug, updated.category.slug);
  redirect("/admin/games");
}

export async function setGameStatusAction(
  id: number,
  status: "draft" | "published" | "disabled",
): Promise<void> {
  await requireAdmin();
  const updated = await db.game.update({
    where: { id },
    data: { status },
    include: { category: true },
  });
  revalidateGame(updated.slug, updated.category.slug);
}

export async function toggleGameFlagAction(
  id: number,
  flag: "featured" | "trending",
  value: boolean,
): Promise<void> {
  await requireAdmin();
  const updated = await db.game.update({
    where: { id },
    data: { [flag]: value },
    include: { category: true },
  });
  revalidateGame(updated.slug, updated.category.slug);
}

export async function deleteGameAction(id: number): Promise<void> {
  await requireAdmin();
  const existing = await db.game.findUnique({
    where: { id },
    include: { category: true },
  });
  if (!existing) return;
  await db.game.delete({ where: { id } });
  revalidateGame(existing.slug, existing.category.slug);
}

/* ---------------- category actions ---------------- */

export interface CategoryFormState {
  errors: Partial<Record<"name" | "slug" | "blurb", string>>;
  values: Record<string, string>;
  message: string;
}

export async function createCategoryAction(
  _prev: CategoryFormState,
  formData: FormData,
): Promise<CategoryFormState> {
  await requireAdmin();
  const values = formValues(formData);
  const { data, errors } = validateCategoryInput(values);
  if (Object.keys(errors).length > 0) {
    return { errors, values, message: "Fix the highlighted problems and try again." };
  }
  const clash = await db.category.findUnique({
    where: { slug: data.slug },
    select: { id: true },
  });
  if (clash) {
    return {
      errors: { slug: "That slug is already taken." },
      values,
      message: "Fix the highlighted problems and try again.",
    };
  }
  await db.category.create({ data });
  revalidatePath("/categories");
  revalidatePath("/admin/categories");
  revalidatePath("/");
  redirect("/admin/categories");
}

export async function deleteCategoryAction(id: number): Promise<string> {
  await requireAdmin();
  const used = await db.game.count({ where: { categoryId: id } });
  if (used > 0) {
    return `Cannot delete: ${used} ${used === 1 ? "game" : "games"} still use this category. Move them first.`;
  }
  await db.category.delete({ where: { id } });
  revalidatePath("/categories");
  revalidatePath("/admin/categories");
  revalidatePath("/");
  return "";
}

export async function updateCategoryAction(
  id: number,
  _prev: CategoryFormState,
  formData: FormData,
): Promise<CategoryFormState> {
  await requireAdmin();
  const values = formValues(formData);
  const { data, errors } = validateCategoryInput(values);
  if (Object.keys(errors).length > 0) {
    return { errors, values, message: "Fix the highlighted problems and try again." };
  }
  const clash = await db.category.findUnique({
    where: { slug: data.slug },
    select: { id: true },
  });
  if (clash && clash.id !== id) {
    return {
      errors: { slug: "That slug is already taken." },
      values,
      message: "Fix the highlighted problems and try again.",
    };
  }
  await db.category.update({ where: { id }, data });
  revalidatePath("/categories");
  revalidatePath("/admin/categories");
  revalidatePath("/");
  redirect("/admin/categories");
}
