import { FastifyInstance } from "fastify";
import crypto from "crypto";
import bcrypt from "bcryptjs";
import prisma from "../../config/database";
import { requireAuth, requirePermission } from "../../middleware/auth";
import { success, error } from "../../utils/response";
import { encrypt } from "../../utils/encryption";
import { getSystemSettings } from "../../services/system-settings";
import { config } from "../../config/env";
import { OTPAuth } from "../../utils/totp";
import * as OTPAuthLib from "otpauth";
import { logAudit } from "../../services/audit";
import { parseBody } from "../../schemas/common";
import {
  TotpBackupSchema,
  TotpEnableSchema,
  TotpDisableSchema,
  TotpRequiredSchema,
} from "../../schemas/auth.schema";

export default async function totpRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  // GET - generate new TOTP secret
  fastify.get("/setup", { preHandler: requireAuth }, async (request, reply) => {
    const settings = await getSystemSettings();
    const companyName =
      (
        await prisma.company_settings.findFirst({
          select: { company_name: true },
        })
      )?.company_name ||
      settings.smtp_from_name ||
      "System";
    const secret = new OTPAuthLib.Secret();
    const totp = new OTPAuthLib.TOTP({
      issuer: companyName,
      label: request.authData!.email,
      secret,
      // Keep the QR/URI params in lockstep with /enable verification and
      // login (all read config.totp.*) so the scanned authenticator produces
      // codes that verify under the same algorithm/digits/period.
      algorithm: config.totp.algorithm,
      digits: config.totp.digits,
      period: config.totp.period,
    });

    return success(reply, {
      secret: secret.base32,
      uri: totp.toString(),
    });
  });

  // POST - enable TOTP
  fastify.post(
    "/enable",
    { preHandler: requireAuth, bodyLimit: 10240 },
    async (request, reply) => {
      const parsed = parseBody(TotpEnableSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const { secret, code, password, current_code } = parsed.data;

      const user = await prisma.users.findUnique({
        where: { id: request.authData!.userId },
      });
      if (!user) return error(reply, "Uživatel nenalezen", 404);

      if (user.totp_enabled) {
        if (!current_code) {
          return error(
            reply,
            "Aktuální TOTP kód je povinný pro změnu 2FA",
            400,
          );
        }
        const verifyResult = OTPAuth.verify(user.totp_secret!, current_code);
        if (!verifyResult.valid) {
          return error(reply, "Neplatný aktuální TOTP kód", 400);
        }
      } else {
        if (!password) {
          return error(reply, "Heslo je povinné pro aktivaci 2FA", 400);
        }
        const valid = await bcrypt.compare(password, user.password_hash);
        if (!valid) {
          return error(reply, "Nesprávné heslo", 400);
        }
      }

      // Verify the new secret with the SAME parameters login verification uses
      // (src/utils/totp.ts → OTPAuth.verify reads config.totp.*). Hardcoding
      // SHA1/6/30 here would let an enrolled secret verify under different
      // params than login → lockout if the config ever diverges.
      const totp = new OTPAuthLib.TOTP({
        secret: OTPAuthLib.Secret.fromBase32(secret),
        algorithm: config.totp.algorithm,
        digits: config.totp.digits,
        period: config.totp.period,
      });

      const delta = totp.validate({ token: code, window: 1 });
      if (delta === null) {
        return error(reply, "Neplatný TOTP kód", 400);
      }

      // Generate 8 backup codes
      const backupCodesPlain: string[] = [];
      const backupCodesHashed: string[] = [];
      for (let i = 0; i < 8; i++) {
        const plainCode = crypto.randomBytes(4).toString("hex").toUpperCase();
        backupCodesPlain.push(plainCode);
        backupCodesHashed.push(
          await bcrypt.hash(plainCode, config.security.bcryptCost),
        );
      }

      const encryptedSecret = encrypt(String(secret));
      await prisma.users.update({
        where: { id: request.authData!.userId },
        data: {
          totp_secret: encryptedSecret,
          totp_enabled: true,
          totp_backup_codes: JSON.stringify(backupCodesHashed),
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "user",
        entityId: request.authData!.userId,
        description: "2FA aktivováno",
      });
      return success(
        reply,
        { backup_codes: backupCodesPlain },
        200,
        "2FA aktivováno",
      );
    },
  );

  // PUT - disable TOTP
  fastify.put(
    "/disable",
    { preHandler: requireAuth },
    async (request, reply) => {
      const parsed = parseBody(TotpDisableSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const { code } = parsed.data;

      const user = await prisma.users.findUnique({
        where: { id: request.authData!.userId },
      });
      if (!user?.totp_secret) {
        return error(reply, "2FA není aktivní", 400);
      }

      const verifyResult = OTPAuth.verify(user.totp_secret, code);
      if (!verifyResult.valid) {
        return error(reply, "Neplatný TOTP kód", 400);
      }

      await prisma.users.update({
        where: { id: request.authData!.userId },
        data: {
          totp_secret: null,
          totp_enabled: false,
          totp_backup_codes: null,
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "user",
        entityId: request.authData!.userId,
        description: "2FA deaktivováno",
      });
      return success(reply, null, 200, "2FA deaktivováno");
    },
  );

  // GET - TOTP status for current user
  fastify.get(
    "/status",
    { preHandler: requireAuth },
    async (request, reply) => {
      const user = await prisma.users.findUnique({
        where: { id: request.authData!.userId },
        select: { totp_enabled: true },
      });

      return success(reply, { totp_enabled: user?.totp_enabled ?? false });
    },
  );

  // GET - check if 2FA is required company-wide
  fastify.get(
    "/required",
    { preHandler: requireAuth },
    async (request, reply) => {
      const settings = await prisma.company_settings.findFirst({
        select: { require_2fa: true },
      });

      return success(reply, { require_2fa: settings?.require_2fa ?? false });
    },
  );

  // POST - toggle mandatory 2FA
  fastify.post(
    "/required",
    {
      preHandler: [requireAuth, requirePermission("settings.system")],
      bodyLimit: 10240,
    },
    async (request, reply) => {
      const parsed = parseBody(TotpRequiredSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const required = parsed.data.required;

      const settings = await prisma.company_settings.findFirst({
        select: { require_2fa: true },
      });
      const oldValue = settings?.require_2fa ?? false;

      await prisma.company_settings.updateMany({
        data: { require_2fa: required },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "company_settings",
        description: `Povinné 2FA změněno z ${oldValue ? "zapnuto" : "vypnuto"} na ${required ? "zapnuto" : "vypnuto"}`,
        oldValues: { require_2fa: oldValue },
        newValues: { require_2fa: required },
      });

      const message = required
        ? "2FA je nyní povinné pro všechny uživatele"
        : "2FA již není povinné";

      return success(reply, null, 200, message);
    },
  );

  // POST - verify backup code (pre-auth, no requireAuth)
  fastify.post(
    "/backup-verify",
    {
      config: {
        rateLimit: {
          max: config.rateLimit.loginTotp,
          timeWindow: "1 minute",
        },
      },
      bodyLimit: 10240,
    },
    async (request, reply) => {
      const parsed = parseBody(TotpBackupSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const { login_token, backup_code: code, remember_me } = parsed.data;
      const rememberMe = remember_me ?? false;

      const tokenHash = crypto
        .createHash("sha256")
        .update(login_token)
        .digest("hex");

      const settings = await getSystemSettings();

      const txResult = await prisma.$transaction(async (tx) => {
        const tokens = await tx.$queryRaw<
          Array<{ id: number; user_id: number; expires_at: Date }>
        >`
          SELECT id, user_id, expires_at FROM totp_login_tokens WHERE token_hash = ${tokenHash} FOR UPDATE
        `;
        const storedToken = tokens[0] ?? null;

        if (!storedToken || new Date(storedToken.expires_at) < new Date()) {
          return { error: "Neplatný nebo expirovaný login token", status: 401 };
        }

        // $queryRaw on MySQL may return BigInt for integer columns
        const storedTokenId = Number(storedToken.id);
        const storedUserId = Number(storedToken.user_id);

        const user = await tx.users.findUnique({
          where: { id: storedUserId },
          include: { roles: true },
        });

        if (!user || !user.totp_backup_codes) {
          return { error: "Uživatel nenalezen", status: 401 };
        }

        if (!user.is_active) {
          return { error: "Účet je deaktivován", status: 401 };
        }

        if (user.locked_until && new Date(user.locked_until) > new Date()) {
          return { error: "Účet je dočasně uzamčen", status: 429 };
        }

        const backupCodes: string[] = JSON.parse(
          user.totp_backup_codes as string,
        );
        let matchIndex = -1;

        for (let i = 0; i < backupCodes.length; i++) {
          const isMatch = await bcrypt.compare(String(code), backupCodes[i]);
          if (isMatch) {
            matchIndex = i;
            break; // early-exit: stop running bcrypt once a code matches
          }
        }

        if (matchIndex === -1) {
          const newFailedAttempts = (user.failed_login_attempts ?? 0) + 1;
          if (newFailedAttempts >= settings.max_login_attempts) {
            await tx.totp_login_tokens.delete({
              where: { id: storedTokenId },
            });
            await tx.users.update({
              where: { id: user.id },
              data: {
                failed_login_attempts: newFailedAttempts,
                locked_until: new Date(
                  Date.now() + settings.lockout_minutes * 60_000,
                ),
              },
            });
            return { error: "Účet je dočasně uzamčen", status: 429 };
          }
          await tx.users.update({
            where: { id: user.id },
            data: { failed_login_attempts: newFailedAttempts },
          });
          return { error: "Neplatný záložní kód", status: 401 };
        }

        await tx.totp_login_tokens.delete({
          where: { id: Number(storedToken.id) },
        });

        backupCodes.splice(matchIndex, 1);
        await tx.users.update({
          where: { id: user.id },
          data: {
            totp_backup_codes: JSON.stringify(backupCodes),
            failed_login_attempts: 0,
            locked_until: null,
            last_login: new Date(),
          },
        });

        return { user };
      });

      if ("error" in txResult) {
        return error(reply, txResult.error!, txResult.status!);
      }

      const user = txResult.user;

      // Create tokens (same as /login/totp flow)
      const { loadAuthData } = await import("../../services/auth");
      const authData = await loadAuthData(user.id);
      if (!authData) {
        return error(reply, "Chyba načítání uživatele", 500);
      }

      const jwt = await import("jsonwebtoken");
      const { config } = await import("../../config/env");

      const accessToken = jwt.default.sign(
        {
          sub: user.id,
          username: user.username,
          role: user.roles?.name ?? null,
        },
        config.jwt.secret,
        { expiresIn: config.jwt.accessTokenExpiry },
      );

      const refreshTokenRaw = crypto.randomBytes(32).toString("hex");
      const refreshTokenHash = crypto
        .createHash("sha256")
        .update(refreshTokenRaw)
        .digest("hex");

      // Mirror /login/totp: honor remember_me so a backup-code login keeps
      // the same session longevity the user asked for at the password step.
      const refreshExpiresIn = rememberMe
        ? config.jwt.refreshTokenRememberExpiry
        : config.jwt.refreshTokenSessionExpiry;

      await prisma.refresh_tokens.create({
        data: {
          user_id: user.id,
          token_hash: refreshTokenHash,
          expires_at: new Date(Date.now() + refreshExpiresIn * 1000),
          remember_me: rememberMe,
          ip_address: request.ip,
          user_agent: request.headers["user-agent"] ?? null,
        },
      });

      reply.setCookie("refresh_token", refreshTokenRaw, {
        httpOnly: true,
        secure: config.isProduction,
        sameSite: "strict",
        path: "/api/admin",
        maxAge: refreshExpiresIn,
      });

      await logAudit({
        request,
        action: "login_backup",
        entityType: "user",
        entityId: user.id,
        description: `Backup code login for user ${user.username}`,
      });

      return success(reply, {
        access_token: accessToken,
        expires_in: config.jwt.accessTokenExpiry,
        user: authData,
      });
    },
  );
}
