import type { Database } from '../db/types.js';
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors.js';
import { normalizeUsername } from '../domain/normalization.js';
import { appendAudit, type AuditContext } from './audit.js';
import { generateTemporaryPassword, hashPassword, validatePasswordStrength, verifyPassword } from './passwords.js';
import { getAuthorization, PERMISSIONS } from './rbac.js';

export interface AuthenticatedUser {
  id: string;
  name: string;
  username: string;
  warehouseId: string | null;
  forcePasswordChange: boolean;
  sessionVersion: number;
}

export class AuthService {
  constructor(private readonly db: Database) {}

  async authenticate(username: string, password: string, audit: AuditContext): Promise<AuthenticatedUser | null> {
    const found = await this.db.query<{
      id: string; name: string; username: string; warehouse_id: string | null; password_hash: string;
      force_password_change: boolean; session_version: number; active: boolean; archived_at: Date | null;
    }>(
      `SELECT id, name, username, warehouse_id, password_hash, force_password_change, session_version, active, archived_at
       FROM users WHERE username_normalized = $1`,
      [normalizeUsername(username)]
    );
    if (!found.rowCount || !found.rows[0]!.active || found.rows[0]!.archived_at || !(await verifyPassword(found.rows[0]!.password_hash, password))) {
      await appendAudit(this.db, audit, { action: 'auth.login_failed', entityType: 'user', entityId: normalizeUsername(username) || 'unknown' });
      return null;
    }
    const user = found.rows[0]!;
    await this.db.transaction(async (tx) => {
      await tx.query('UPDATE users SET last_login_at = now(), updated_at = now() WHERE id = $1', [user.id]);
      await appendAudit(tx, { ...audit, actorId: user.id }, { action: 'auth.login_success', entityType: 'user', entityId: user.id });
    });
    return {
      id: user.id, name: user.name, username: user.username, warehouseId: user.warehouse_id,
      forcePasswordChange: user.force_password_change, sessionVersion: user.session_version
    };
  }

  async changeOwnPassword(
    userId: string,
    currentPassword: string,
    newPassword: string,
    audit: AuditContext
  ): Promise<number> {
    const errors = validatePasswordStrength(newPassword);
    if (errors.length) throw new ConflictError(errors.join(' '), 'WEAK_PASSWORD');
    return this.db.transaction(async (tx) => {
      const user = await tx.query<{ password_hash: string; session_version: number }>('SELECT password_hash, session_version FROM users WHERE id = $1 FOR UPDATE', [userId]);
      if (!user.rowCount) throw new NotFoundError('کاربر یافت نشد.');
      if (!(await verifyPassword(user.rows[0]!.password_hash, currentPassword))) throw new ForbiddenError('رمز عبور فعلی صحیح نیست.');
      const next = user.rows[0]!.session_version + 1;
      await tx.query(
        `UPDATE users SET password_hash = $2, force_password_change = false, session_version = $3, updated_at = now() WHERE id = $1`,
        [userId, await hashPassword(newPassword), next]
      );
      await appendAudit(tx, { ...audit, actorId: userId }, {
        action: 'auth.password_changed', entityType: 'user', entityId: userId, newValues: { sessionVersion: next }
      });
      return next;
    });
  }

  async resetPassword(adminId: string, userId: string, audit: AuditContext): Promise<string> {
    return this.db.transaction(async (tx) => {
      const auth = await getAuthorization(tx, adminId);
      if (!auth.permissions.has(PERMISSIONS.USERS_MANAGE)) throw new ForbiddenError();
      const target = await tx.query<{ id: string }>('SELECT id FROM users WHERE id = $1 FOR UPDATE', [userId]);
      if (!target.rowCount) throw new NotFoundError('کاربر یافت نشد.');
      const temporary = generateTemporaryPassword();
      await tx.query(
        `UPDATE users SET password_hash = $2, force_password_change = true,
          session_version = session_version + 1, updated_at = now() WHERE id = $1`,
        [userId, await hashPassword(temporary)]
      );
      await appendAudit(tx, { ...audit, actorId: adminId, actorRole: auth.roles.join(',') }, {
        action: 'auth.password_reset', entityType: 'user', entityId: userId,
        newValues: { forcedChange: true, sessionsInvalidated: true }
      });
      return temporary;
    });
  }
}

