/**
 * سرویس احراز هویت.
 * - رمزها با Argon2id ذخیره می‌شوند.
 * - توکن نشست فقط به‌صورت هش (SHA-256) در پایگاه داده نگهداری می‌شود؛ نسخه خام
 *   تنها داخل کوکی HttpOnly قرار می‌گیرد.
 * - شمارش ورودهای ناموفق و قفل موقت حساب برای مقابله با Brute Force.
 */
import crypto from 'node:crypto';
import argon2 from 'argon2';
import { and, eq, isNull } from 'drizzle-orm';
import { getDb } from '../../database/client.js';
import { users, sessions } from '../../database/schema/index.js';
import { config } from '../../config/index.js';
import { Errors } from '../../common/errors.js';
import { loadGrants, type Principal } from '../rbac/service.js';
import { recordAudit, AuditActions } from '../audit/service.js';

const ARGON_OPTIONS: argon2.Options = {
  type: argon2.argon2id,
  memoryCost: 19456, // ~19MB مطابق توصیه OWASP
  timeCost: 2,
  parallelism: 1,
};

export function hashToken(token: string): string {
  return crypto.createHash('sha256').update(token).digest('hex');
}

export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, ARGON_OPTIONS);
}

export async function verifyPassword(hash: string, password: string): Promise<boolean> {
  try {
    return await argon2.verify(hash, password);
  } catch {
    return false;
  }
}

export type LoginContext = { ip?: string; userAgent?: string };

export async function login(email: string, password: string, ctx: LoginContext) {
  const db = getDb();
  const [user] = await db.select().from(users).where(eq(users.email, email.toLowerCase().trim()));

  // پیام خطا عمداً یکسان است تا وجود/عدم وجود حساب افشا نشود
  const genericError = Errors.unauthorized('ایمیل یا گذرواژه نادرست است.');

  if (!user) {
    await recordAudit({ action: AuditActions.LOGIN_FAILED, actorLabel: email, ip: ctx.ip, userAgent: ctx.userAgent });
    throw genericError;
  }

  // اگر حساب به دلیل تلاش‌های ناموفق قفل شده باشد
  if (user.lockedUntil && user.lockedUntil > new Date()) {
    throw Errors.tooMany('به دلیل تلاش‌های ناموفق، حساب شما موقتاً قفل شده است. کمی بعد دوباره تلاش کنید.');
  }
  if (user.status !== 'active') throw Errors.forbidden('این حساب غیرفعال است.');

  const ok = await verifyPassword(user.passwordHash, password);
  if (!ok) {
    const failed = user.failedLoginCount + 1;
    const locked = failed >= config.security.maxFailedLogins
      ? new Date(Date.now() + config.security.lockMinutes * 60_000)
      : null;
    await db.update(users).set({ failedLoginCount: failed, lockedUntil: locked }).where(eq(users.id, user.id));
    await recordAudit({
      action: AuditActions.LOGIN_FAILED, actorId: user.id, actorLabel: user.email,
      ip: ctx.ip, userAgent: ctx.userAgent, metadata: { failed },
    });
    throw genericError;
  }

  await db.update(users).set({ failedLoginCount: 0, lockedUntil: null, lastLoginAt: new Date() }).where(eq(users.id, user.id));
  const session = await createSession(user.id, ctx);
  await recordAudit({ action: AuditActions.LOGIN, actorId: user.id, actorLabel: user.email, ip: ctx.ip, userAgent: ctx.userAgent });
  return { user, ...session };
}

export async function createSession(userId: string, ctx: LoginContext, rotatedFrom?: string) {
  const db = getDb();
  const token = crypto.randomBytes(32).toString('base64url');
  const csrfToken = crypto.randomBytes(24).toString('base64url');
  const expiresAt = new Date(Date.now() + config.security.sessionTtlHours * 3600_000);
  const [row] = await db
    .insert(sessions)
    .values({
      userId,
      tokenHash: hashToken(token),
      rotatedFrom: rotatedFrom ?? null,
      userAgent: ctx.userAgent ?? null,
      ip: ctx.ip ?? null,
      expiresAt,
    })
    .returning();
  return { token, csrfToken, sessionId: row.id, expiresAt };
}

/** چرخش توکن نشست: نشست قدیمی باطل و نشست جدید زنجیر می‌شود. */
export async function rotateSession(oldToken: string, ctx: LoginContext) {
  const db = getDb();
  const [current] = await db.select().from(sessions).where(eq(sessions.tokenHash, hashToken(oldToken)));
  if (!current || current.revokedAt || current.expiresAt < new Date()) throw Errors.unauthorized();
  await db.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, current.id));
  return createSession(current.userId, ctx, current.id);
}

export async function logout(token: string) {
  const db = getDb();
  await db.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.tokenHash, hashToken(token)));
}

export async function resolvePrincipal(token: string | undefined): Promise<Principal | null> {
  if (!token) return null;
  const db = getDb();
  const [row] = await db
    .select({ session: sessions, user: users })
    .from(sessions)
    .innerJoin(users, eq(users.id, sessions.userId))
    .where(and(eq(sessions.tokenHash, hashToken(token)), isNull(sessions.revokedAt)));
  if (!row) return null;
  if (row.session.expiresAt < new Date()) return null;
  if (row.user.status !== 'active') return null;
  const grants = await loadGrants(row.user.id);
  return { userId: row.user.id, displayName: row.user.displayName, email: row.user.email, grants };
}

export async function register(input: { email: string; password: string; displayName: string; phone?: string }) {
  const db = getDb();
  const email = input.email.toLowerCase().trim();
  const [existing] = await db.select().from(users).where(eq(users.email, email));
  if (existing) throw Errors.conflict('این ایمیل قبلاً ثبت شده است.');
  if (input.password.length < 10) throw Errors.validation('گذرواژه باید حداقل ۱۰ نویسه باشد.');
  const passwordHash = await hashPassword(input.password);
  const [user] = await db
    .insert(users)
    .values({ email, passwordHash, displayName: input.displayName, phone: input.phone ?? null })
    .returning();
  return user;
}
