import { Injectable, UnauthorizedException, ForbiddenException, ConflictException, ServiceUnavailableException } from "@nestjs/common";
import { JwtService, type JwtSignOptions } from "@nestjs/jwt";
import bcrypt from "bcryptjs";
import { OAuth2Client } from "google-auth-library";
import { AppRole, ApiEnvelope } from "../../contracts";
import { PrismaService } from "../../common/prisma/prisma.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { randomUUID } from "node:crypto";

const TOKEN_ISSUER = "mawjod-api";
const ACCESS_AUDIENCE = "mawjod-client";
const REFRESH_AUDIENCE = "mawjod-refresh";
const DUMMY_PASSWORD_HASH = "$2b$12$sslf/hJ8TkXDvGe/Qp6OuO5oMruIlzxK1qhZ8rSo6wLwwUhKzaKhy";
const ADMIN_ROLES = new Set<AppRole>(["SUPPORT", "MODERATOR", "ADMIN", "SUPER_ADMIN"]);
const ADMIN_LOGIN_ATTEMPT_LIMIT = 5;
const ADMIN_LOGIN_LOCK_DURATION_MS = 15 * 60 * 1000;

@Injectable()
export class AuthService {
  private readonly googleOAuthClient = new OAuth2Client();

  constructor(private readonly prisma: PrismaService, private readonly jwt: JwtService) {}

  private getJwtSecrets() {
    const jwtAccessSecret = process.env.JWT_ACCESS_SECRET;
    const jwtRefreshSecret = process.env.JWT_REFRESH_SECRET;
    if (!jwtAccessSecret || !jwtRefreshSecret) {
      throw new Error("JWT secrets are not configured");
    }
    return { jwtAccessSecret, jwtRefreshSecret };
  }

  async register(body: RegisterDto): Promise<ApiEnvelope<{ accessToken: string; refreshToken: string; userId: string }>> {
    const { jwtAccessSecret, jwtRefreshSecret } = this.getJwtSecrets();
    const registration = await this.prisma.appSetting.findUnique({ where: { key: "registrationEnabled" } });
    if (registration?.value === false) throw new ForbiddenException("registration_disabled");
    const passwordHash = await bcrypt.hash(body.password, 12);
    const existing = await this.prisma.user.findUnique({
      where: { email: body.email.toLowerCase() },
    });
    if (existing) {
      throw new ConflictException("email_already_registered");
    }

    const user = await this.prisma.user.create({
      data: {
        email: body.email.toLowerCase(),
        fullName: body.fullName,
        role: "CUSTOMER",
        passwordHash,
        permissions: [],
      },
    });

    return this.createSession(user.id, user.role, user.permissions, jwtAccessSecret, jwtRefreshSecret);
  }

  async login(body: LoginDto): Promise<ApiEnvelope<{ accessToken: string; refreshToken: string; userId: string }>> {
    const { jwtAccessSecret, jwtRefreshSecret } = this.getJwtSecrets();
    const user = await this.prisma.user.findUnique({ where: { email: body.email.toLowerCase() } });
    const valid = await bcrypt.compare(body.password, user?.passwordHash ?? DUMMY_PASSWORD_HASH);
    const now = new Date();
    const privileged = Boolean(user && ADMIN_ROLES.has(user.role));
    const locked = Boolean(privileged && user?.adminLockedUntil && user.adminLockedUntil > now);
    if (!user || !valid || !user.isActive || ["SUSPENDED", "BANNED", "DELETED"].includes(user.status)) {
      if (privileged && !locked && user?.isActive && !["SUSPENDED", "BANNED", "DELETED"].includes(user.status)) {
        await this.recordAdminLoginFailure(user.id, user.adminFailedLoginAttempts, user.adminLockedUntil, now);
      }
      throw new UnauthorizedException("invalid_credentials");
    }
    if (locked) throw new UnauthorizedException("invalid_credentials");
    if (user.role === "GUEST") {
      throw new ForbiddenException("not_authorized");
    }
    if (privileged && (user.adminFailedLoginAttempts > 0 || user.adminLockedUntil)) {
      await this.prisma.user.update({
        where: { id: user.id },
        data: { adminFailedLoginAttempts: 0, adminLockedUntil: null },
      });
    }
    return this.createSession(user.id, user.role, user.permissions, jwtAccessSecret, jwtRefreshSecret);
  }

  private async recordAdminLoginFailure(userId: string, previousAttempts: number, lockedUntil: Date | null, now: Date) {
    const attempts = lockedUntil && lockedUntil <= now ? 1 : previousAttempts + 1;
    await this.prisma.user.update({
      where: { id: userId },
      data: {
        adminFailedLoginAttempts: attempts >= ADMIN_LOGIN_ATTEMPT_LIMIT ? 0 : attempts,
        adminLockedUntil: attempts >= ADMIN_LOGIN_ATTEMPT_LIMIT
          ? new Date(now.getTime() + ADMIN_LOGIN_LOCK_DURATION_MS)
          : null,
      },
    });
  }

  async loginWithGoogle(idToken: string): Promise<ApiEnvelope<{ accessToken: string; refreshToken: string; userId: string }>> {
    const { jwtAccessSecret, jwtRefreshSecret } = this.getJwtSecrets();
    const audiences = (process.env.GOOGLE_OAUTH_CLIENT_IDS ?? process.env.GOOGLE_WEB_CLIENT_ID ?? "")
      .split(",")
      .map((value) => value.trim())
      .filter(Boolean);
    if (!audiences.length) throw new ServiceUnavailableException("google_auth_not_configured");

    let payload;
    try {
      const ticket = await this.googleOAuthClient.verifyIdToken({ idToken, audience: audiences });
      payload = ticket.getPayload();
    } catch {
      throw new UnauthorizedException("invalid_google_token");
    }

    const email = payload?.email?.trim().toLowerCase();
    const subject = payload?.sub?.trim();
    if (!email || !subject || payload?.email_verified !== true || email.length > 254) {
      throw new UnauthorizedException("invalid_google_token");
    }

    let user = await this.prisma.user.findUnique({ where: { googleSubject: subject } });
    if (!user) {
      const existing = await this.prisma.user.findUnique({ where: { email } });
      if (existing) throw new ConflictException("google_account_link_required");
      const registration = await this.prisma.appSetting.findUnique({ where: { key: "registrationEnabled" } });
      if (registration?.value === false) throw new ForbiddenException("registration_disabled");
      const fallbackName = email.slice(0, email.indexOf("@"));
      const fullName = (payload.name?.trim() || fallbackName).slice(0, 100);
      user = await this.prisma.user.create({
        data: {
          email,
          googleSubject: subject,
          fullName,
          role: "CUSTOMER",
          passwordHash: await bcrypt.hash(randomUUID(), 12),
          permissions: [],
          avatarUrl: payload.picture ?? null,
        },
      });
    }

    if (!user.isActive || ["SUSPENDED", "BANNED", "DELETED"].includes(user.status)) {
      throw new UnauthorizedException("invalid_credentials");
    }
    if (user.role === "GUEST") throw new ForbiddenException("not_authorized");
    return this.createSession(user.id, user.role, user.permissions, jwtAccessSecret, jwtRefreshSecret);
  }

  async refresh(refreshToken: string): Promise<ApiEnvelope<{ accessToken: string; refreshToken: string; userId: string }>> {
    const { jwtRefreshSecret, jwtAccessSecret } = this.getJwtSecrets();
    let payload: { sub: string; jti?: string; tokenVersion?: number; tokenType?: string };
    try {
      payload = await this.jwt.verifyAsync<typeof payload>(refreshToken, {
        secret: jwtRefreshSecret,
        issuer: TOKEN_ISSUER,
        audience: REFRESH_AUDIENCE,
        algorithms: ["HS256"],
      });
    } catch {
      throw new UnauthorizedException("invalid_refresh_token");
    }
    if (typeof payload.sub !== "string" || !payload.sub || !payload.jti || payload.tokenType !== "refresh") throw new UnauthorizedException("invalid_refresh_token");
    const user = await this.prisma.user.findUnique({ where: { id: payload.sub } });
    if (!user?.isActive || ["SUSPENDED", "BANNED", "DELETED"].includes(user.status) || (payload.tokenVersion ?? 0) !== user.tokenVersion) throw new UnauthorizedException("session_revoked");
    return this.createSession(user.id, user.role, user.permissions, jwtAccessSecret, jwtRefreshSecret, payload.jti);
  }

  async logout(refreshToken: string) {
    const { jwtRefreshSecret } = this.getJwtSecrets();
    try {
      const payload = await this.jwt.verifyAsync<{ sub?: string; jti?: string; tokenType?: string }>(refreshToken, {
        secret: jwtRefreshSecret,
        issuer: TOKEN_ISSUER,
        audience: REFRESH_AUDIENCE,
      });
      if (payload.sub && payload.jti && payload.tokenType === "refresh") {
        await this.prisma.authSession.updateMany({
          where: { id: payload.jti, userId: payload.sub, revokedAt: null },
          data: { revokedAt: new Date() },
        });
      }
    } catch {
      // Logout is intentionally idempotent and does not disclose token state.
    }
    return { loggedOut: true };
  }

  private async createSession(
    userId: string,
    role: AppRole,
    permissions: string[],
    jwtAccessSecret: string,
    jwtRefreshSecret: string,
    previousSessionId?: string,
  ) {
    const user = await this.prisma.user.findUniqueOrThrow({ where: { id: userId }, select: { tokenVersion: true } });
    const accessToken = await this.jwt.signAsync({ sub: userId, role, permissions, tokenVersion: user.tokenVersion, tokenType: "access" }, {
      secret: jwtAccessSecret,
      expiresIn: (process.env.JWT_ACCESS_EXPIRES || "15m") as JwtSignOptions["expiresIn"],
      issuer: TOKEN_ISSUER,
      audience: ACCESS_AUDIENCE,
      jwtid: randomUUID(),
      algorithm: "HS256",
    });
    const sessionId = randomUUID();
    const refreshToken = await this.jwt.signAsync({ sub: userId, tokenVersion: user.tokenVersion, tokenType: "refresh" }, {
      secret: jwtRefreshSecret,
      expiresIn: (process.env.JWT_REFRESH_EXPIRES || "7d") as JwtSignOptions["expiresIn"],
      issuer: TOKEN_ISSUER,
      audience: REFRESH_AUDIENCE,
      jwtid: sessionId,
      algorithm: "HS256",
    });
    const decoded = this.jwt.decode<{ exp?: number }>(refreshToken);
    if (!decoded?.exp) throw new Error("Refresh token expiration is missing");
    const expiresAt = new Date(decoded.exp * 1000);
    const now = new Date();
    try {
      await this.prisma.$transaction(async (tx) => {
        if (ADMIN_ROLES.has(role) && !previousSessionId) {
          await tx.authSession.updateMany({
            where: { userId, revokedAt: null },
            data: { revokedAt: now },
          });
        }
        if (previousSessionId) {
          const claimed = await tx.authSession.updateMany({
            where: { id: previousSessionId, userId, revokedAt: null, expiresAt: { gt: now } },
            data: { revokedAt: now, lastUsedAt: now },
          });
          if (!claimed.count) throw new UnauthorizedException("refresh_token_reused");
        }
        await tx.authSession.deleteMany({ where: { userId, OR: [{ expiresAt: { lte: now } }, { revokedAt: { not: null } }] } });
        await tx.authSession.create({ data: { id: sessionId, userId, expiresAt } });
      });
    } catch (error) {
      if (error instanceof UnauthorizedException && previousSessionId) {
        await this.prisma.$transaction([
          this.prisma.authSession.updateMany({ where: { userId, revokedAt: null }, data: { revokedAt: now } }),
          this.prisma.user.update({ where: { id: userId }, data: { tokenVersion: { increment: 1 } } }),
        ]);
      }
      throw error;
    }
    return {
      data: { accessToken, refreshToken, userId },
      requestId: "auth-session",
    };
  }
}
