import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import {
  getAuthRequestMetadata,
  recordUserAuthActivity,
} from "@/lib/auth-activity";
import {
  buildMailEntryUrl,
  getMailAbsoluteUrl,
  getMailAppUrl,
  isLocalOrigin,
  resolveMailAppUrlFromHeaders,
  resolveOriginFromHeaders,
} from "@/lib/mail-urls";
import { getSessionSnapshotByEmail } from "@/lib/official-mail";
import { ensureOfficialMailSchema, getDbPool } from "@/lib/db";
import {
  createAppSession,
  getAppSessionUserByToken,
  readAppBearerToken,
  revokeAppSession,
} from "@/lib/app-auth";
import type { SupportedLocale } from "@/lib/i18n-config";

const SESSION_COOKIE = "official_mail_session";
const USER_COOKIE = "official_mail_user";
const REMEMBER_COOKIE = "official_mail_remember";
const DEVICE_COOKIE = "official_mail_device";
const REMEMBER_DURATION_MS = 1000 * 60 * 60 * 24 * 30;
const WEB_SESSION_DURATION_MS = 1000 * 60 * 60 * 8;

type RememberDeviceRow = RowDataPacket & {
  device_id_hash: string;
  email: string;
  id: number;
  token_hash: string;
};

export type SessionUser = {
  email: string;
  companyName: string;
  displayName: string;
  mailConfigured: boolean;
  domain: string;
  mailbox: string;
  preferredLocale: SupportedLocale | null;
};

async function resolveCookieDomain() {
  const requestHeaders = await headers();
  const currentOrigin = resolveOriginFromHeaders(requestHeaders, getMailAppUrl());

  if (isLocalOrigin(currentOrigin)) {
    return undefined;
  }

  const appUrl = process.env.NEXT_PUBLIC_APP_URL;

  if (!appUrl) {
    return undefined;
  }

  try {
    const hostname = new URL(appUrl).hostname;

    if (hostname === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
      return undefined;
    }

    return hostname.startsWith(".") ? hostname : `.${hostname}`;
  } catch {
    return undefined;
  }
}

function createSha256Hex(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function isHashMatch(value: string, expectedHash: string) {
  const actual = Buffer.from(createSha256Hex(value), "hex");
  const expected = Buffer.from(expectedHash, "hex");
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}

function parseRememberToken(value: string | undefined) {
  const [selector, validator, ...rest] = String(value ?? "").split(".");

  if (
    rest.length > 0 ||
    !/^[a-f0-9]{32}$/.test(selector ?? "") ||
    !/^[A-Za-z0-9_-]{43}$/.test(validator ?? "")
  ) {
    return null;
  }

  return { selector, validator };
}

async function revokeRememberToken(value: string | undefined) {
  const parsed = parseRememberToken(value);

  if (!parsed) {
    return;
  }

  await ensureOfficialMailSchema();
  await getDbPool().query(
    `
      UPDATE user_remember_devices
      SET revoked_at = COALESCE(revoked_at, NOW()), updated_at = NOW()
      WHERE selector = ?
    `,
    [parsed.selector],
  );
}

async function getRememberedSessionUser(
  requestHeaders: Pick<Headers, "get">,
) {
  const cookieStore = await cookies();
  const parsed = parseRememberToken(cookieStore.get(REMEMBER_COOKIE)?.value);
  const deviceId = cookieStore.get(DEVICE_COOKIE)?.value ?? "";

  if (!parsed || !/^[A-Za-z0-9_-]{43}$/.test(deviceId)) {
    return null;
  }

  await ensureOfficialMailSchema();
  const [rows] = await getDbPool().query<RememberDeviceRow[]>(
    `
      SELECT
        rd.id,
        rd.token_hash,
        rd.device_id_hash,
        u.email
      FROM user_remember_devices rd
      INNER JOIN users u ON u.id = rd.user_id
      WHERE rd.selector = ?
        AND u.account_deleted_at IS NULL
        AND rd.revoked_at IS NULL
        AND rd.expires_at > NOW()
      LIMIT 1
    `,
    [parsed.selector],
  );
  const rememberedDevice = rows[0];

  if (
    !rememberedDevice ||
    !isHashMatch(parsed.validator, rememberedDevice.token_hash) ||
    !isHashMatch(deviceId, rememberedDevice.device_id_hash)
  ) {
    return null;
  }

  const [dailyUseResult] = await getDbPool().query<ResultSetHeader>(
    `
      UPDATE user_remember_devices
      SET last_used_at = NOW(), updated_at = NOW()
      WHERE id = ?
        AND (last_used_at IS NULL OR last_used_at < CURDATE())
    `,
    [rememberedDevice.id],
  );

  if (dailyUseResult.affectedRows === 0) {
    await getDbPool().query(
      `
        UPDATE user_remember_devices
        SET last_used_at = NOW(), updated_at = NOW()
        WHERE id = ?
          AND (last_used_at IS NULL OR last_used_at < DATE_SUB(NOW(), INTERVAL 1 HOUR))
      `,
      [rememberedDevice.id],
    );
  } else {
    await recordUserAuthActivity({
      ...getAuthRequestMetadata(requestHeaders, "web"),
      eventType: "login",
      identityEmail: rememberedDevice.email,
      lookup: "login_email",
      outcome: "success",
    });
  }

  return getSessionSnapshotByEmail(rememberedDevice.email);
}

export async function getSessionUser() {
  const cookieStore = await cookies();
  const sessionToken = cookieStore.get(SESSION_COOKIE)?.value;
  const requestHeaders = await headers();

  if (sessionToken) {
    const sessionUser = await getAppSessionUserByToken(sessionToken, requestHeaders);
    if (sessionUser) return sessionUser;
  }

  const appUser = await getAppSessionUserByToken(
    readAppBearerToken(requestHeaders),
    requestHeaders,
  );

  if (appUser) {
    return appUser;
  }

  return getRememberedSessionUser(requestHeaders);
}

export async function requireSessionUser() {
  const user = await getSessionUser();

  if (!user) {
    redirect("/login");
  }

  return user;
}

export async function saveSessionUser(user: SessionUser) {
  const cookieStore = await cookies();
  const previousToken = cookieStore.get(SESSION_COOKIE)?.value;
  const session = await createAppSession(user.email, WEB_SESSION_DURATION_MS);
  const expires = new Date(Date.now() + WEB_SESSION_DURATION_MS);
  const domain = await resolveCookieDomain();

  cookieStore.set(SESSION_COOKIE, session.token, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    priority: "high",
    path: "/",
    expires,
    domain,
  });
  cookieStore.delete({ name: USER_COOKIE, path: "/", domain });
  await revokeAppSession(previousToken);
}

export async function configureRememberDevice(email: string, enabled: boolean) {
  const cookieStore = await cookies();
  const currentToken = cookieStore.get(REMEMBER_COOKIE)?.value;
  const domain = await resolveCookieDomain();

  await revokeRememberToken(currentToken);

  if (!enabled) {
    cookieStore.delete({ name: REMEMBER_COOKIE, path: "/", domain });
    cookieStore.delete({ name: DEVICE_COOKIE, path: "/", domain });
    return;
  }

  await ensureOfficialMailSchema();
  const normalizedEmail = email.trim().toLowerCase();
  const [userRows] = await getDbPool().query<(RowDataPacket & { id: number })[]>(
    `SELECT id FROM users WHERE email = ? AND account_deleted_at IS NULL LIMIT 1`,
    [normalizedEmail],
  );
  const userId = Number(userRows[0]?.id ?? 0);

  if (!userId) {
    throw new Error("remember-device-user-not-found");
  }

  const existingDeviceId = cookieStore.get(DEVICE_COOKIE)?.value ?? "";
  const deviceId = /^[A-Za-z0-9_-]{43}$/.test(existingDeviceId)
    ? existingDeviceId
    : randomBytes(32).toString("base64url");
  const selector = randomBytes(16).toString("hex");
  const validator = randomBytes(32).toString("base64url");
  const expires = new Date(Date.now() + REMEMBER_DURATION_MS);
  const requestHeaders = await headers();
  const userAgent = requestHeaders.get("user-agent")?.slice(0, 512) || null;

  await getDbPool().query(
    `
      DELETE FROM user_remember_devices
      WHERE expires_at <= NOW()
         OR (revoked_at IS NOT NULL AND revoked_at < DATE_SUB(NOW(), INTERVAL 30 DAY))
    `,
  );
  await getDbPool().query(
    `
      UPDATE user_remember_devices
      SET revoked_at = COALESCE(revoked_at, NOW()), updated_at = NOW()
      WHERE user_id = ? AND device_id_hash = ? AND revoked_at IS NULL
    `,
    [userId, createSha256Hex(deviceId)],
  );
  await getDbPool().query(
    `
      INSERT INTO user_remember_devices (
        user_id,
        selector,
        token_hash,
        device_id_hash,
        user_agent,
        expires_at,
        last_used_at
      ) VALUES (?, ?, ?, ?, ?, ?, NOW())
    `,
    [
      userId,
      selector,
      createSha256Hex(validator),
      createSha256Hex(deviceId),
      userAgent,
      expires,
    ],
  );

  const cookieOptions = {
    domain,
    expires,
    httpOnly: true,
    path: "/" as const,
    priority: "high" as const,
    sameSite: "lax" as const,
    secure: process.env.NODE_ENV === "production",
  };
  cookieStore.set(DEVICE_COOKIE, deviceId, cookieOptions);
  cookieStore.set(REMEMBER_COOKIE, `${selector}.${validator}`, cookieOptions);
}

export async function revokeAllRememberDevicesByEmail(email: string) {
  await ensureOfficialMailSchema();
  await getDbPool().query(
    `
      UPDATE user_remember_devices rd
      INNER JOIN users u ON u.id = rd.user_id
      SET rd.revoked_at = COALESCE(rd.revoked_at, NOW()), rd.updated_at = NOW()
      WHERE u.email = ? AND rd.revoked_at IS NULL
    `,
    [email.trim().toLowerCase()],
  );
}

export async function clearRememberDeviceCookies() {
  const cookieStore = await cookies();
  const domain = await resolveCookieDomain();
  cookieStore.delete({ name: REMEMBER_COOKIE, path: "/", domain });
  cookieStore.delete({ name: DEVICE_COOKIE, path: "/", domain });
}

export async function clearSession() {
  const cookieStore = await cookies();
  const domain = await resolveCookieDomain();

  await revokeAppSession(cookieStore.get(SESSION_COOKIE)?.value);
  await revokeRememberToken(cookieStore.get(REMEMBER_COOKIE)?.value);
  cookieStore.delete({ name: SESSION_COOKIE, path: "/", domain });
  cookieStore.delete({ name: USER_COOKIE, path: "/", domain });
  cookieStore.delete({ name: REMEMBER_COOKIE, path: "/", domain });
  cookieStore.delete({ name: DEVICE_COOKIE, path: "/", domain });
}

export function getMailEntryUrl(
  user?: Pick<SessionUser, "mailConfigured"> | null,
) {
  return buildMailEntryUrl(user);
}

export async function getMailEntryUrlForCurrentRequest(
  user?: Pick<SessionUser, "mailConfigured"> | null,
) {
  const requestHeaders = await headers();
  return buildMailEntryUrl(user, resolveMailAppUrlFromHeaders(requestHeaders));
}

export async function getMailAuthUrlForCurrentRequest(path: string) {
  const requestHeaders = await headers();
  const baseUrl = resolveMailAppUrlFromHeaders(requestHeaders);
  return getMailAbsoluteUrl(path, baseUrl);
}

export async function redirectToCanonicalMailAuthHostIfNeeded(
  pathname: string,
  search = "",
) {
  const requestHeaders = await headers();
  const currentOrigin = resolveOriginFromHeaders(requestHeaders);
  const canonicalMailUrl = getMailAppUrl();

  if (currentOrigin === canonicalMailUrl) {
    return;
  }

  if (resolveMailAppUrlFromHeaders(requestHeaders) === currentOrigin) {
    return;
  }

  redirect(getMailAbsoluteUrl(`${pathname}${search}`, canonicalMailUrl));
}
