import type { FastifyInstance } from "fastify";
import { randomBytes, randomUUID } from "node:crypto";
import { and, eq, isNull } from "drizzle-orm";
import { schema } from "@app/database";
import {
  loginInputSchema,
  refreshInputSchema,
  registerInputSchema,
  changePasswordInputSchema,
  forgotPasswordInputSchema,
  resetPasswordInputSchema,
  verifyEmailInputSchema,
  totpVerifyInputSchema,
} from "@app/validation";
import { trialEndsAt } from "@app/subscriptions";
import { BRAND, DEFAULT_LOCALE_PREFERENCES, localeDefaultsForRegion } from "@app/shared-types";
import { renderMail } from "../lib/mailer.js";
import { loadLocalePreferences } from "../lib/localeContext.js";
import { errors, parse } from "../lib/errors.js";
import { hashPassword, verifyPassword } from "../lib/passwords.js";
import { verifyTotp } from "../lib/totp.js";
import { audit, sha256 } from "../lib/helpers.js";

/**
 * Auth: registrering, inloggning, roterande refresh-tokens, utloggning.
 * Refresh-tokens lagras hashade; återanvändning av roterad token
 * ogiltigförklarar hela familjen (token theft detection).
 */
export async function authRoutes(app: FastifyInstance) {
  const strictLimit = { config: { rateLimit: { max: 10, timeWindow: "1 minute" } } };

  app.post("/v1/auth/register", strictLimit, async (req, reply) => {
    const input = parse(registerInputSchema, req.body);

    const [existing] = await app.db
      .select({ id: schema.users.id })
      .from(schema.users)
      .where(eq(schema.users.email, input.email))
      .limit(1);
    if (existing) throw errors.conflict("E-postadressen är redan registrerad.");

    const [user] = await app.db
      .insert(schema.users)
      .values({ email: input.email, displayName: input.displayName, locale: input.locale })
      .returning();
    if (!user) throw errors.internal();

    await app.db
      .insert(schema.userCredentials)
      .values({ userId: user.id, passwordHash: await hashPassword(input.password) });
    await app.db.insert(schema.userPreferences).values({ userId: user.id }).onConflictDoNothing();
    await app.db
      .insert(schema.userHealthProfiles)
      .values({ userId: user.id })
      .onConflictDoNothing();

    // Product analytics: legitimate interest with opt-out (spec §8, §33).
    const consentNow = new Date();
    await app.db.insert(schema.userConsents).values({
      userId: user.id,
      kind: "product_analytics",
      status: "granted",
      grantedAt: consentNow,
      updatedAt: consentNow,
    });

    // 7 dagars trial utan kort startar direkt (spec §46)
    const now = new Date();
    await app.db
      .insert(schema.trials)
      .values({ userId: user.id, startedAt: now, endsAt: trialEndsAt(now) });

    await audit(app.db, {
      actorUserId: user.id,
      action: "auth.register",
      ip: req.ip,
      correlationId: req.correlationId,
    });

    // D-031: enhetens språk följer med registreringen → locale-preferenser skapas
    // FÖRE välkomstmejlet, så att spanjorens första mejl kommer på spanska.
    const regionFromLocale = input.locale.split("-")[1]?.toUpperCase();
    const localeDefaults = regionFromLocale
      ? localeDefaultsForRegion(regionFromLocale)
      : DEFAULT_LOCALE_PREFERENCES;
    await app.db
      .insert(schema.userLocalePreferences)
      .values({
        userId: user.id,
        languageTag: input.locale,
        regionCode: localeDefaults.regionCode,
        timeZone: localeDefaults.timeZone,
        measurementSystem: localeDefaults.measurementSystem,
        temperatureUnit: localeDefaults.temperatureUnit,
        currencyCode: localeDefaults.currencyCode,
      })
      .onConflictDoNothing();

    // E-postverifiering (icke-blockerande): mejlet skickas, kontot fungerar direkt.
    await sendVerificationMail(app, user.id, user.email);

    const tokens = await issueTokens(app, user.id, user.role, req.headers["user-agent"], req.ip);
    return reply.status(201).send({
      user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role },
      ...tokens,
    });
  });

  app.post("/v1/auth/login", strictLimit, async (req, reply) => {
    const input = parse(loginInputSchema, req.body);

    const [user] = await app.db
      .select()
      .from(schema.users)
      .where(and(eq(schema.users.email, input.email), isNull(schema.users.deletedAt)))
      .limit(1);
    const [creds] = user
      ? await app.db
          .select()
          .from(schema.userCredentials)
          .where(eq(schema.userCredentials.userId, user.id))
          .limit(1)
      : [];

    const ok = user && creds ? await verifyPassword(input.password, creds.passwordHash) : false;
    if (!ok || !user) {
      // Konstant svar oavsett om kontot finns – ingen user enumeration.
      throw errors.unauthorized("Fel e-post eller lösenord.");
    }

    // Admin-2FA (step-up): rätt lösenord räcker inte om TOTP är aktiverat.
    const [totp] = await app.db
      .select()
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, user.id))
      .limit(1);
    if (totp?.enabledAt) {
      const preAuthToken = app.jwt.sign(
        { sub: user.id, type: "preauth" },
        { expiresIn: 300 }, // 5 minuter att ange koden
      );
      await audit(app.db, {
        actorUserId: user.id,
        action: "auth.login_totp_required",
        ip: req.ip,
        correlationId: req.correlationId,
      });
      return reply.send({ totpRequired: true, preAuthToken });
    }

    await audit(app.db, {
      actorUserId: user.id,
      action: "auth.login",
      ip: req.ip,
      correlationId: req.correlationId,
    });
    const tokens = await issueTokens(app, user.id, user.role, req.headers["user-agent"], req.ip);
    return reply.send({
      user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role },
      ...tokens,
    });
  });

  /** Steg 2 av admin-inloggning: preauth-token + TOTP-kod → riktiga tokens. */
  app.post("/v1/auth/totp-verify", strictLimit, async (req, reply) => {
    const input = parse(totpVerifyInputSchema, req.body);
    let payload: { sub?: string; type?: string };
    try {
      payload = app.jwt.verify(input.preAuthToken);
    } catch {
      throw errors.unauthorized("Ogiltig eller utgången inloggning. Börja om.");
    }
    if (payload.type !== "preauth" || !payload.sub) throw errors.unauthorized();

    const [totp] = await app.db
      .select()
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, payload.sub))
      .limit(1);
    if (!totp?.enabledAt) throw errors.unauthorized();

    const step = verifyTotp(totp.secretBase32, input.code);
    if (step == null || (totp.lastUsedStep != null && step <= totp.lastUsedStep)) {
      await audit(app.db, {
        actorUserId: payload.sub,
        action: "auth.totp_failed",
        ip: req.ip,
      });
      throw errors.unauthorized("Fel engångskod.");
    }
    await app.db
      .update(schema.adminTotp)
      .set({ lastUsedStep: step })
      .where(eq(schema.adminTotp.userId, payload.sub));

    const [user] = await app.db
      .select()
      .from(schema.users)
      .where(and(eq(schema.users.id, payload.sub), isNull(schema.users.deletedAt)))
      .limit(1);
    if (!user) throw errors.unauthorized();

    await audit(app.db, {
      actorUserId: user.id,
      action: "auth.login",
      metadata: { mfa: true },
      ip: req.ip,
      correlationId: req.correlationId,
    });
    const tokens = await issueTokens(app, user.id, user.role, req.headers["user-agent"], req.ip, {
      mfa: true,
    });
    return reply.send({
      user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role },
      ...tokens,
    });
  });

  /** Bekräfta e-postadress (länken i välkomstmejlet). Engångs, 24 h TTL. */
  app.post("/v1/auth/verify-email", strictLimit, async (req, reply) => {
    const input = parse(verifyEmailInputSchema, req.body);
    const [stored] = await app.db
      .select()
      .from(schema.emailVerificationTokens)
      .where(eq(schema.emailVerificationTokens.tokenHash, sha256(input.token)))
      .limit(1);
    if (!stored || stored.usedAt || stored.expiresAt < new Date()) {
      throw errors.unauthorized("Ogiltig eller utgången verifieringslänk. Begär en ny.");
    }
    await app.db
      .update(schema.emailVerificationTokens)
      .set({ usedAt: new Date() })
      .where(eq(schema.emailVerificationTokens.id, stored.id));
    await app.db
      .update(schema.users)
      .set({ emailVerifiedAt: new Date(), updatedAt: new Date() })
      .where(eq(schema.users.id, stored.userId));
    await audit(app.db, { actorUserId: stored.userId, action: "auth.email_verified", ip: req.ip });
    return reply.send({ ok: true });
  });

  /** Skicka nytt verifieringsmejl (inloggad, ej redan verifierad). */
  app.post(
    "/v1/auth/resend-verification",
    { preHandler: [app.authenticate], ...strictLimit },
    async (req, reply) => {
      const [user] = await app.db
        .select({ email: schema.users.email, verifiedAt: schema.users.emailVerifiedAt })
        .from(schema.users)
        .where(eq(schema.users.id, req.userId))
        .limit(1);
      if (!user) throw errors.unauthorized();
      if (!user.verifiedAt) await sendVerificationMail(app, req.userId, user.email);
      return reply.send({ ok: true });
    },
  );

  app.post("/v1/auth/refresh", strictLimit, async (req, reply) => {
    const input = parse(refreshInputSchema, req.body);
    const tokenHash = sha256(input.refreshToken);

    const [stored] = await app.db
      .select()
      .from(schema.refreshTokens)
      .where(eq(schema.refreshTokens.tokenHash, tokenHash))
      .limit(1);

    if (!stored) throw errors.unauthorized("Ogiltig refresh-token.");

    if (stored.revokedAt) {
      // Token-återanvändning → hela familjen ogiltigförklaras.
      await app.db
        .update(schema.refreshTokens)
        .set({ revokedAt: new Date() })
        .where(eq(schema.refreshTokens.familyId, stored.familyId));
      await audit(app.db, {
        actorUserId: stored.userId,
        action: "auth.refresh_reuse_detected",
        metadata: { familyId: stored.familyId },
        ip: req.ip,
      });
      throw errors.unauthorized("Sessionen har återkallats. Logga in igen.");
    }
    if (stored.expiresAt < new Date()) throw errors.unauthorized("Sessionen har gått ut.");

    const [user] = await app.db
      .select()
      .from(schema.users)
      .where(and(eq(schema.users.id, stored.userId), isNull(schema.users.deletedAt)))
      .limit(1);
    if (!user) throw errors.unauthorized();

    // Rotera: revokera gamla, utfärda ny i samma familj.
    const next = await issueTokens(
      app,
      user.id,
      user.role,
      req.headers["user-agent"],
      req.ip,
      stored.familyId,
    );
    await app.db
      .update(schema.refreshTokens)
      .set({ revokedAt: new Date(), replacedByTokenId: next.refreshTokenId })
      .where(eq(schema.refreshTokens.id, stored.id));

    return reply.send({
      accessToken: next.accessToken,
      refreshToken: next.refreshToken,
      accessTokenExpiresIn: next.accessTokenExpiresIn,
    });
  });

  app.post("/v1/auth/logout", { preHandler: [app.authenticate] }, async (req, reply) => {
    await app.db
      .update(schema.refreshTokens)
      .set({ revokedAt: new Date() })
      .where(
        and(eq(schema.refreshTokens.userId, req.userId), isNull(schema.refreshTokens.revokedAt)),
      );
    await audit(app.db, { actorUserId: req.userId, action: "auth.logout", ip: req.ip });
    return reply.send({ ok: true });
  });

  /**
   * Lösenordsåterställning steg 1 (spec §56: säker kontohantering).
   * Svarar ALLTID { ok: true } – avslöjar aldrig om kontot finns
   * (anti-enumeration). Token: 32 slumpbytes, lagras sha256-hashad,
   * 30 min TTL, engångsbruk; tidigare oanvända tokens ogiltigförklaras.
   */
  app.post("/v1/auth/forgot-password", strictLimit, async (req, reply) => {
    const input = parse(forgotPasswordInputSchema, req.body);
    const [user] = await app.db
      .select({ id: schema.users.id, email: schema.users.email })
      .from(schema.users)
      .where(eq(schema.users.email, input.email))
      .limit(1);

    if (user) {
      // Ogiltigförklara tidigare oanvända tokens.
      await app.db
        .update(schema.passwordResetTokens)
        .set({ usedAt: new Date() })
        .where(
          and(
            eq(schema.passwordResetTokens.userId, user.id),
            isNull(schema.passwordResetTokens.usedAt),
          ),
        );
      const token = randomBytes(32).toString("hex");
      await app.db.insert(schema.passwordResetTokens).values({
        userId: user.id,
        tokenHash: sha256(token),
        expiresAt: new Date(Date.now() + 30 * 60_000),
      });
      const languageTag = (await loadLocalePreferences(app.db, user.id)).languageTag;
      const mail = renderMail("auth.password_reset", languageTag, {
        email: user.email,
        link: `${BRAND.urlScheme}://reset-password?token=${token}`,
      });
      await app.mailer.send({ to: user.email, ...mail });
      await audit(app.db, {
        actorUserId: user.id,
        action: "auth.password_reset_requested",
        ip: req.ip,
      });
    }
    // Samma svar oavsett – och ingen tidsskillnad stor nog att mäta via rate limit.
    return reply.send({ ok: true });
  });

  /** Lösenordsåterställning steg 2: token + nytt lösenord. Engångsbruk. */
  app.post("/v1/auth/reset-password", strictLimit, async (req, reply) => {
    const input = parse(resetPasswordInputSchema, req.body);
    const [stored] = await app.db
      .select()
      .from(schema.passwordResetTokens)
      .where(eq(schema.passwordResetTokens.tokenHash, sha256(input.token)))
      .limit(1);
    if (!stored || stored.usedAt || stored.expiresAt < new Date()) {
      throw errors.unauthorized("Ogiltig eller utgången återställningslänk. Begär en ny.");
    }
    await app.db
      .update(schema.passwordResetTokens)
      .set({ usedAt: new Date() })
      .where(eq(schema.passwordResetTokens.id, stored.id));
    await app.db
      .update(schema.userCredentials)
      .set({ passwordHash: await hashPassword(input.newPassword), passwordUpdatedAt: new Date() })
      .where(eq(schema.userCredentials.userId, stored.userId));
    // Logga ut ALLA sessioner – ett återställt konto börjar om från noll.
    await app.db
      .update(schema.refreshTokens)
      .set({ revokedAt: new Date() })
      .where(
        and(eq(schema.refreshTokens.userId, stored.userId), isNull(schema.refreshTokens.revokedAt)),
      );
    await audit(app.db, {
      actorUserId: stored.userId,
      action: "auth.password_reset_completed",
      ip: req.ip,
    });
    return reply.send({ ok: true });
  });

  app.post(
    "/v1/auth/change-password",
    { preHandler: [app.authenticate], ...strictLimit },
    async (req, reply) => {
      const input = parse(changePasswordInputSchema, req.body);
      const [creds] = await app.db
        .select()
        .from(schema.userCredentials)
        .where(eq(schema.userCredentials.userId, req.userId))
        .limit(1);
      if (!creds || !(await verifyPassword(input.currentPassword, creds.passwordHash))) {
        throw errors.unauthorized("Fel nuvarande lösenord.");
      }
      await app.db
        .update(schema.userCredentials)
        .set({ passwordHash: await hashPassword(input.newPassword), passwordUpdatedAt: new Date() })
        .where(eq(schema.userCredentials.userId, req.userId));
      // Logga ut alla andra sessioner.
      await app.db
        .update(schema.refreshTokens)
        .set({ revokedAt: new Date() })
        .where(
          and(eq(schema.refreshTokens.userId, req.userId), isNull(schema.refreshTokens.revokedAt)),
        );
      await audit(app.db, { actorUserId: req.userId, action: "auth.change_password", ip: req.ip });
      return reply.send({ ok: true });
    },
  );
}

async function issueTokens(
  app: FastifyInstance,
  userId: string,
  role: string,
  userAgent: string | undefined,
  ip: string | undefined,
  familyIdOrOpts?: string | { mfa?: boolean },
  opts?: { mfa?: boolean },
) {
  const familyId = typeof familyIdOrOpts === "string" ? familyIdOrOpts : undefined;
  const mfa = (typeof familyIdOrOpts === "object" ? familyIdOrOpts.mfa : opts?.mfa) ?? false;
  const accessToken = app.jwt.sign({ sub: userId, role, type: "access", ...(mfa ? { mfa } : {}) });
  const refreshToken = randomUUID() + "." + randomUUID();
  const family = familyId ?? randomUUID();
  const [row] = await app.db
    .insert(schema.refreshTokens)
    .values({
      userId,
      tokenHash: sha256(refreshToken),
      familyId: family,
      expiresAt: new Date(Date.now() + app.config.JWT_REFRESH_TTL_SECONDS * 1000),
      userAgent: userAgent?.slice(0, 300) ?? null,
      ip: ip ?? null,
    })
    .returning({ id: schema.refreshTokens.id });
  return {
    accessToken,
    refreshToken,
    refreshTokenId: row!.id,
    accessTokenExpiresIn: app.config.JWT_ACCESS_TTL_SECONDS,
  };
}

/** Skapa verifieringstoken + skicka mejl på användarens språk (24 h TTL). */
async function sendVerificationMail(app: FastifyInstance, userId: string, email: string) {
  await app.db
    .update(schema.emailVerificationTokens)
    .set({ usedAt: new Date() })
    .where(
      and(
        eq(schema.emailVerificationTokens.userId, userId),
        isNull(schema.emailVerificationTokens.usedAt),
      ),
    );
  const token = randomBytes(32).toString("hex");
  await app.db.insert(schema.emailVerificationTokens).values({
    userId,
    tokenHash: sha256(token),
    expiresAt: new Date(Date.now() + 24 * 3600_000),
  });
  const languageTag = (await loadLocalePreferences(app.db, userId)).languageTag;
  const mail = renderMail("auth.verify_email", languageTag, {
    email,
    link: `${BRAND.urlScheme}://verify-email?token=${token}`,
  });
  await app.mailer.send({ to: email, ...mail });
}
