import { NextResponse } from "next/server";
import {
  createUser,
  findUserByEmail,
  isGoogleOnlyAccount,
  isUniqueViolation,
  issueVerificationLink,
} from "@/lib/auth/accounts";
import { isDisposableEmail, normaliseEmail } from "@/lib/auth/email";
import { equaliseTiming, hashPassword } from "@/lib/auth/password";
import { clientIp, rateLimit } from "@/lib/auth/rateLimit";
import { authError, rateLimited, requireDatabase } from "@/lib/auth/http";
import { requireCsrf } from "@/lib/auth/csrf";
import { validateEmail, validatePassword } from "@/lib/auth/validation";
import {
  logSecurityEventAsync,
  SecurityEventType,
} from "@/lib/auth/securityEvents";
import { isProduction, smtp } from "@/lib/auth/config";
import { MAX_JSON_BODY_BYTES } from "@/lib/limits";
import { readJsonSized } from "@/lib/http/requestSize";
import {
  signupDecision,
  signupResponseBody,
  type SignupAccountState,
} from "@/lib/auth/neutral";
import type { User } from "@/lib/schema/db";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

interface SignupBody {
  name?: string;
  email?: string;
  password?: string;
  lang?: "de" | "en";
}

/**
 * Creates an unverified account and mails a confirmation link. No session is
 * issued here on purpose: the account is inert until the link is clicked, which
 * is what makes bulk fake signups pointless.
 *
 * Anti-enumeration: the response is the same 200 `{ status: "verification_sent" }`
 * whether the address is new, already confirmed, or a Google-only account — the
 * endpoint must not reveal which addresses are registered. The only thing that
 * differs under the hood is whether a real mail is produced: never for already
 * verified / Google-only accounts, always (with a fresh link) for new or
 * never-confirmed ones. The no-mail path burns one scrypt's worth of time
 * (`equaliseTiming`) so its faster branch is not measurable over the wire.
 */
export async function POST(request: Request) {
  const dbDown = await requireDatabase();
  if (dbDown) return dbDown;

  const csrfBlocked = requireCsrf(request);
  if (csrfBlocked) return csrfBlocked;

  // Refuse before writing anything: an account created without a deliverable
  // confirmation link could never be activated, only left behind as a stray row.
  // The guard runs before any lookup, so the refusal is identical for all addresses.
  if (isProduction && !smtp.isConfigured) {
    console.error("[auth] signup blocked — SMTP is not configured in production");
    return authError("mail_failed", 503);
  }

  const parsed = await readJsonSized<SignupBody>(request, MAX_JSON_BODY_BYTES);
  if (!parsed.ok) {
    // Oversized bodies are rejected with 413 before anything is buffered;
    // parse failures keep the existing invalid_request/400 contract.
    return parsed.response.status === 413
      ? parsed.response
      : authError("invalid_request", 400);
  }
  const body = parsed.body;
  if (!body?.email || !body?.password) return authError("invalid_request", 400);

  const ip = clientIp(request);
  const userAgent = request.headers.get("user-agent");
  const ipLimit = rateLimit(`signup:ip:${ip}`, 5, 60 * 60 * 1000);
  if (!ipLimit.allowed) return rateLimited(ipLimit.retryAfter);

  const lang = body.lang === "de" ? "de" : "en";
  const email = body.email.trim();

  // Server-side re-validation: the client checks are UX, never a control.
  if (validateEmail(email, false)) {
    logSecurityEventAsync({
      type: SecurityEventType.SIGNUP_FAILED,
      email,
      ip,
      userAgent,
      metadata: { reason: "invalid_email", route: "/api/auth/signup" },
    });
    return authError("invalid_email", 400);
  }
  if (validatePassword(body.password, false, { requireStrength: true })) {
    logSecurityEventAsync({
      type: SecurityEventType.SIGNUP_FAILED,
      email,
      ip,
      userAgent,
      metadata: { reason: "weak_password", route: "/api/auth/signup" },
    });
    return authError("weak_password", 400);
  }
  if (isDisposableEmail(email)) {
    logSecurityEventAsync({
      type: SecurityEventType.SIGNUP_FAILED,
      email,
      ip,
      userAgent,
      metadata: { reason: "disposable_email", route: "/api/auth/signup" },
    });
    return authError("disposable_email", 400);
  }

  const emailKey = normaliseEmail(email);
  if (!emailKey) return authError("invalid_email", 400);

  const emailLimit = rateLimit(`signup:email:${emailKey}`, 3, 60 * 60 * 1000);
  if (!emailLimit.allowed) return rateLimited(emailLimit.retryAfter);

  try {
    const existing = await findUserByEmail(email);

    if (existing) {
      // Address already registered: the wire response stays neutral (no
      // existence oracle), but the audit trail still records the attempt.
      logSecurityEventAsync({
        type: SecurityEventType.SIGNUP_FAILED,
        userId: existing.id,
        email: existing.email ?? emailKey,
        ip,
        userAgent,
        metadata: { reason: "already_registered", route: "/api/auth/signup" },
      });
      return await neutralSignupResponse(existing, signupStateFor(existing), lang);
    }

    const user = await createUser({
      email,
      name: body.name,
      passwordHash: await hashPassword(body.password),
    });

    logSecurityEventAsync({
      type: SecurityEventType.SIGNUP,
      userId: user.id,
      email: user.email ?? emailKey,
      ip,
      userAgent,
      metadata: { route: "/api/auth/signup" },
    });

    return await neutralSignupResponse(user, "new", lang);
  } catch (error) {
    // Lost the race against a concurrent signup for the same address. Answer
    // exactly as if the account had existed before this request — neutral, and
    // the same for every address.
    if (isUniqueViolation(error)) {
      const raced = await findUserByEmail(email).catch(() => null);
      if (raced) {
        logSecurityEventAsync({
          type: SecurityEventType.SIGNUP_FAILED,
          userId: raced.id,
          email: raced.email ?? emailKey,
          ip,
          userAgent,
          metadata: { reason: "already_registered", route: "/api/auth/signup" },
        });
        return await neutralSignupResponse(raced, signupStateFor(raced), lang);
      }
      // The winner vanished between the insert and this read; reply as if the
      // confirmation mail was on its way rather than creating a second account.
      logSecurityEventAsync({
        type: SecurityEventType.SIGNUP_FAILED,
        email: emailKey,
        ip,
        userAgent,
        metadata: { reason: "race", route: "/api/auth/signup" },
      });
      return NextResponse.json(signupResponseBody(signupDecision("new"), undefined));
    }

    console.error("[auth] signup failed", error);
    if (error instanceof Error && error.message.includes("SMTP")) {
      logSecurityEventAsync({
        type: SecurityEventType.SIGNUP_FAILED,
        email: emailKey,
        ip,
        userAgent,
        metadata: { reason: "mail_failed", route: "/api/auth/signup" },
      });
      return authError("mail_failed", 500);
    }
    logSecurityEventAsync({
      type: SecurityEventType.SIGNUP_FAILED,
      email: emailKey,
      ip,
      userAgent,
      metadata: { reason: "server_error", route: "/api/auth/signup" },
    });
    return authError("server_error", 500);
  }
}

function signupStateFor(existing: User | null): SignupAccountState {
  if (!existing) return "new";
  if (isGoogleOnlyAccount(existing)) return "google_only";
  if (existing.emailVerifiedAt) return "verified";
  return "unverified";
}

/**
 * The single place signup turns a decision into a response, so every branch is
 * guaranteed to produce the identical `{ status: "verification_sent" }` body.
 */
async function neutralSignupResponse(
  user: User,
  state: SignupAccountState,
  lang: "de" | "en"
): Promise<NextResponse> {
  const decision = signupDecision(state);

  if (decision.sendMail) {
    // Fresh link for a brand-new or never-confirmed account. `devLink` appears
    // only in the development fallback when no SMTP is configured.
    const mail = await issueVerificationLink(user, lang);
    return NextResponse.json(signupResponseBody(decision, mail.devLink));
  }

  // Already verified or Google-only: no new mail, identical answer. Burn one
  // scrypt's worth of time so this faster branch cannot be timed apart from the
  // mail-producing paths.
  await equaliseTiming();
  return NextResponse.json(signupResponseBody(decision, undefined));
}
