import { NextResponse } from "next/server";
import { changePasswordForUser } from "@/lib/auth/accounts";
import { requireCsrf } from "@/lib/auth/csrf";
import { authError, rateLimited, requireDatabase } from "@/lib/auth/http";
import { clientIp, rateLimit } from "@/lib/auth/rateLimit";
import {
  logSecurityEventAsync,
  SecurityEventType,
} from "@/lib/auth/securityEvents";
import { createSession, getCurrentUser } from "@/lib/auth/session";
import { validatePassword } from "@/lib/auth/validation";
import { MAX_JSON_BODY_BYTES } from "@/lib/limits";
import { readJsonSized } from "@/lib/http/requestSize";

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

interface ChangePasswordBody {
  currentPassword?: string;
  newPassword?: string;
  remember?: boolean;
}

/**
 * Logged-in password change with full session rotation.
 *
 * Security semantics — why every old token dies:
 *
 *  1. The CURRENT password is re-checked server-side (`changePasswordForUser`),
 *     so a leaked session cookie alone cannot change the credential: the caller
 *     must prove they know the existing password.
 *  2. On success `changePasswordForUser` deletes EVERY session row for the
 *     account. Any device that was already signed in — including one an
 *     attacker gained access through — is signed out instantly, with no
 *     re-login required to evict it.
 *  3. The current device is then handed a BRAND-NEW session token via
 *     `createSession`. The user stays signed in where they are, but their old
 *     cookie value is dead; only the freshly issued one authenticates.
 *  4. This endpoint mutates credentials, so it is CSRF-guarded (`requireCsrf`:
 *     origin allow-list + double-submit token) before anything else runs.
 *
 * `remember: true` is chosen deliberately: a password change keeps the same
 * long-lived session policy as a "keep me signed in" login instead of silently
 * downgrading the user's session lifetime to the default 12h. (The body may
 * carry a `remember` flag, but the server keeps the decision deterministic.)
 */
export async function POST(request: Request) {
  const csrfBlocked = requireCsrf(request);
  if (csrfBlocked) return csrfBlocked;

  const dbDown = await requireDatabase();
  if (dbDown) return dbDown;

  try {
    const user = await getCurrentUser(request);
    if (!user) return authError("unauthorized", 401);

    const parsed = await readJsonSized<ChangePasswordBody>(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?.currentPassword || !body?.newPassword) {
      return authError("invalid_request", 400);
    }

    // Server-side re-validation: the client checks are UX, never a control.
    if (validatePassword(body.newPassword, false, { requireStrength: true })) {
      return authError("weak_password", 400);
    }

    // Wrong-password guessing is itself a credential attack — slow it down.
    const ip = clientIp(request);
    const ipLimit = rateLimit(`change:ip:${ip}`, 10, 15 * 60 * 1000);
    if (!ipLimit.allowed) return rateLimited(ipLimit.retryAfter);

    const outcome = await changePasswordForUser(
      user.id,
      body.currentPassword,
      body.newPassword
    );

    // Wire codes come from the shared AuthErrorCode vocabulary (errors.ts):
    // "wrong_password" (400) and "unauthorized" (401) are both defined there.
    if (outcome.status === "no_password") return authError("use_google", 409);
    if (outcome.status === "wrong_password") {
      // Wrong-current-password attempts are credential guessing, so they land
      // in the audit trail as failed-login events for monitoring.
      logSecurityEventAsync({
        type: SecurityEventType.LOGIN_FAILED,
        userId: user.id,
        email: user.email,
        ip,
        userAgent: request.headers.get("user-agent"),
        metadata: { route: "/api/auth/change-password", reason: "wrong_password" },
      });
      return authError("wrong_password", 400);
    }
    if (outcome.status === "invalid") return authError("unauthorized", 401);

    logSecurityEventAsync({
      type: SecurityEventType.PASSWORD_CHANGED,
      userId: user.id,
      email: user.email,
      ip,
      userAgent: request.headers.get("user-agent"),
      metadata: { route: "/api/auth/change-password" },
    });

    // Session rotation: every old token was deleted above; re-issue a fresh one
    // for the current device so the user is not signed out of the browser they
    // are sitting in front of.
    await createSession(user.id, {
      remember: true,
      userAgent: request.headers.get("user-agent"),
      ip,
    });

    return NextResponse.json({ status: "password_changed" });
  } catch (error) {
    console.error("[auth] password change failed", error);
    return authError("server_error", 500);
  }
}
