import type { FastifyInstance } from "fastify";
import { and, desc, eq, ilike, sql } from "drizzle-orm";
import { schema } from "@app/database";
import { z } from "zod";
import { adminGrantInputSchema, totpCodeInputSchema } from "@app/validation";
import { errors, parse } from "../lib/errors.js";
import { audit } from "../lib/helpers.js";
import { generateTotpSecret, otpauthUrl, verifyTotp } from "../lib/totp.js";
import { BRAND } from "@app/shared-types";

/** Adminpanelens API (spec §57). Alla anrop kräver admin-roll och auditloggas. */
export async function adminRoutes(app: FastifyInstance) {
  const admin = { preHandler: [app.requireAdmin] };

  // --- Users ---
  app.get("/admin/v1/users", admin, async (req) => {
    const q = z
      .object({
        search: z.string().max(100).optional(),
        limit: z.coerce.number().int().min(1).max(100).default(50),
        offset: z.coerce.number().int().min(0).default(0),
      })
      .parse(req.query);
    const conditions = q.search ? ilike(schema.users.email, `%${q.search}%`) : undefined;
    const users = await app.db
      .select({
        id: schema.users.id,
        email: schema.users.email,
        displayName: schema.users.displayName,
        role: schema.users.role,
        onboardingCompleted: schema.users.onboardingCompleted,
        createdAt: schema.users.createdAt,
        deletedAt: schema.users.deletedAt,
      })
      .from(schema.users)
      .where(conditions)
      .orderBy(desc(schema.users.createdAt))
      .limit(q.limit)
      .offset(q.offset);
    return { users };
  });

  // --- Receptmoderering (spec §35 steg 4) ---
  app.get("/admin/v1/moderation/recipes", admin, async () => {
    const pending = await app.db
      .select()
      .from(schema.recipes)
      .where(sql`${schema.recipes.status} IN ('submitted', 'ai_checked', 'in_moderation')`)
      .orderBy(schema.recipes.createdAt)
      .limit(50);
    return { recipes: pending };
  });

  app.post("/admin/v1/moderation/recipes/:id", admin, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const body = z
      .object({
        action: z.enum(["approve", "reject", "request_changes"]),
        note: z.string().max(1000).optional(),
      })
      .parse(req.body);

    const status =
      body.action === "approve" ? "published" : body.action === "reject" ? "rejected" : "draft";
    const [recipe] = await app.db
      .update(schema.recipes)
      .set({ status, moderationNote: body.note ?? null, updatedAt: new Date() })
      .where(eq(schema.recipes.id, params.id))
      .returning();
    if (!recipe) throw errors.notFound();

    if (body.action === "approve" && recipe.creatorUserId) {
      await app.db
        .insert(schema.creatorStats)
        .values({ userId: recipe.creatorUserId, publishedRecipes: 1 })
        .onConflictDoUpdate({
          target: schema.creatorStats.userId,
          set: {
            publishedRecipes: sql`${schema.creatorStats.publishedRecipes} + 1`,
            updatedAt: new Date(),
          },
        });
    }

    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: `moderation.recipe_${body.action}`,
      targetType: "recipe",
      targetId: params.id,
      metadata: { note: body.note },
    });
    return recipe;
  });

  // --- Feature flags (spec §57) ---
  app.get("/admin/v1/flags", admin, async () => {
    return {
      flags: await app.db.select().from(schema.featureFlags).orderBy(schema.featureFlags.key),
    };
  });

  app.put("/admin/v1/flags/:key", admin, async (req) => {
    const params = z.object({ key: z.string().max(60) }).parse(req.params);
    const body = z
      .object({
        enabled: z.boolean(),
        rolloutPercent: z.number().int().min(0).max(100).default(100),
        descriptionSv: z.string().max(300).optional(),
      })
      .parse(req.body);
    const [flag] = await app.db
      .insert(schema.featureFlags)
      .values({ key: params.key, ...body })
      .onConflictDoUpdate({
        target: schema.featureFlags.key,
        set: { ...body, updatedAt: new Date() },
      })
      .returning();
    app.flags.invalidate();
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "flags.updated",
      targetType: "feature_flag",
      targetId: params.key,
      metadata: body,
    });
    return flag;
  });

  // --- Subscriptions ---
  app.get("/admin/v1/subscriptions", admin, async () => {
    const subs = await app.db
      .select()
      .from(schema.subscriptions)
      .orderBy(desc(schema.subscriptions.updatedAt))
      .limit(100);
    return { subscriptions: subs };
  });

  app.post("/admin/v1/subscriptions/grant", admin, async (req) => {
    const input = parse(adminGrantInputSchema, req.body);
    if (input.plan === "free")
      throw errors.badRequest("Använd delete i stället för att sätta free.");
    const [sub] = await app.db
      .insert(schema.subscriptions)
      .values({
        userId: input.userId,
        provider: "promo",
        productId: `promo_${input.plan}`,
        plan: input.plan,
        status: "active",
        purchasedAt: new Date(),
        expiresAt: new Date(Date.now() + input.days * 86_400_000),
        lastVerifiedAt: new Date(),
      })
      .returning();
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "subscription.granted",
      targetType: "user",
      targetId: input.userId,
      metadata: { plan: input.plan, days: input.days, reason: input.reason },
    });
    return sub;
  });

  // --- Jobb & systemhälsa (spec §57–58) ---
  app.get("/admin/v1/jobs/overview", admin, async () => {
    const scanStats = await app.db
      .select({ status: schema.scanJobs.status, count: sql<number>`count(*)` })
      .from(schema.scanJobs)
      .groupBy(schema.scanJobs.status);
    const queueCounts = await app.jobQueue.getJobCounts();
    const unpublishedEvents = await app.db
      .select({ count: sql<number>`count(*)` })
      .from(schema.domainEvents)
      .where(sql`${schema.domainEvents.publishedAt} IS NULL`);
    return {
      scanJobs: scanStats,
      queue: queueCounts,
      outboxPending: Number(unpublishedEvents[0]?.count ?? 0),
    };
  });

  app.get("/admin/v1/system/health", admin, async () => {
    const checks: Record<string, { ok: boolean; detail?: string }> = {};
    try {
      await app.db.execute(sql`SELECT 1`);
      checks.database = { ok: true };
    } catch (err) {
      checks.database = { ok: false, detail: String(err) };
    }
    try {
      const pong = await app.redis.ping();
      checks.redis = { ok: pong === "PONG" };
    } catch (err) {
      checks.redis = { ok: false, detail: String(err) };
    }
    checks.aamos = await app.aamos.healthCheck();
    checks.connectors = { ok: true, detail: `${app.connectors.list().length} registrerade` };
    return { checks, timestamp: new Date().toISOString() };
  });

  // --- Audit logs ---
  app.get("/admin/v1/audit-logs", admin, async (req) => {
    const q = z
      .object({
        action: z.string().max(60).optional(),
        limit: z.coerce.number().int().min(1).max(200).default(100),
      })
      .parse(req.query);
    const conditions = q.action ? ilike(schema.auditLogs.action, `%${q.action}%`) : undefined;
    const logs = await app.db
      .select()
      .from(schema.auditLogs)
      .where(conditions)
      .orderBy(desc(schema.auditLogs.createdAt))
      .limit(q.limit);
    return { logs };
  });

  // --- AI-korrigeringar & evals (spec §33–34) ---
  app.get("/admin/v1/ai/corrections", admin, async () => {
    const corrections = await app.db
      .select()
      .from(schema.aiCorrections)
      .orderBy(desc(schema.aiCorrections.createdAt))
      .limit(100);
    return { corrections };
  });

  app.get("/admin/v1/ai/eval-runs", admin, async () => {
    const runs = await app.db
      .select()
      .from(schema.aiEvalRuns)
      .orderBy(desc(schema.aiEvalRuns.createdAt))
      .limit(50);
    return { runs };
  });

  // --- Admin-2FA (TOTP, RFC 6238) ---

  /**
   * Starta 2FA-setup: nytt secret + otpauth-URL för autentiseringsappen.
   * OBS: kräver befintlig admin-session; kan köras om tills enable bekräftats.
   */
  app.post("/admin/v1/2fa/setup", admin, async (req) => {
    const [existing] = await app.db
      .select({ enabledAt: schema.adminTotp.enabledAt })
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, req.userId))
      .limit(1);
    if (existing?.enabledAt) {
      throw errors.badRequest("2FA är redan aktiverat. Inaktivera först för att byta secret.");
    }
    const secret = generateTotpSecret();
    const [user] = await app.db
      .select({ email: schema.users.email })
      .from(schema.users)
      .where(eq(schema.users.id, req.userId))
      .limit(1);
    await app.db
      .insert(schema.adminTotp)
      .values({ userId: req.userId, secretBase32: secret })
      .onConflictDoUpdate({
        target: schema.adminTotp.userId,
        set: { secretBase32: secret, enabledAt: null, lastUsedStep: null },
      });
    return {
      secret,
      otpauthUrl: otpauthUrl(secret, user?.email ?? "admin", `${BRAND.name} Admin`),
    };
  });

  /** Bekräfta setup med första koden – först nu börjar tvånget gälla. */
  app.post("/admin/v1/2fa/enable", admin, async (req) => {
    const body = parse(totpCodeInputSchema, req.body);
    const [row] = await app.db
      .select()
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, req.userId))
      .limit(1);
    if (!row) throw errors.badRequest("Kör setup först.");
    if (row.enabledAt) return { enabled: true };
    const step = verifyTotp(row.secretBase32, body.code);
    if (step == null)
      throw errors.unauthorized("Fel engångskod – kontrollera appen och försök igen.");
    await app.db
      .update(schema.adminTotp)
      .set({ enabledAt: new Date(), lastUsedStep: step })
      .where(eq(schema.adminTotp.userId, req.userId));
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "admin.2fa_enabled",
    });
    return { enabled: true };
  });

  /** Inaktivera 2FA (kräver giltig kod – aldrig bara en session). */
  app.post("/admin/v1/2fa/disable", admin, async (req) => {
    const body = parse(totpCodeInputSchema, req.body);
    const [row] = await app.db
      .select()
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, req.userId))
      .limit(1);
    if (!row?.enabledAt) return { enabled: false };
    if (verifyTotp(row.secretBase32, body.code) == null) {
      throw errors.unauthorized("Fel engångskod.");
    }
    await app.db.delete(schema.adminTotp).where(eq(schema.adminTotp.userId, req.userId));
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "admin.2fa_disabled",
    });
    return { enabled: false };
  });

  /** Status för inloggad admin (visas i adminpanelen). */
  app.get("/admin/v1/2fa/status", admin, async (req) => {
    const [row] = await app.db
      .select({ enabledAt: schema.adminTotp.enabledAt })
      .from(schema.adminTotp)
      .where(eq(schema.adminTotp.userId, req.userId))
      .limit(1);
    return { enabled: Boolean(row?.enabledAt) };
  });

  // --- Receptöversättningar (i18n-spec §13–14, M3) ---

  /** Beställ AI-utkast för ett recept och språk. Idempotent (upsert i worker). */
  app.post("/admin/v1/recipes/:id/translate", admin, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const body = z.object({ languageTag: z.string().min(2).max(35) }).parse(req.body);
    const [recipe] = await app.db
      .select({ id: schema.recipes.id })
      .from(schema.recipes)
      .where(eq(schema.recipes.id, params.id))
      .limit(1);
    if (!recipe) throw errors.notFound("Receptet finns inte.");
    await app.jobQueue.add("TRANSLATE_RECIPE", {
      jobType: "TRANSLATE_RECIPE",
      recipeId: params.id,
      targetLanguageTag: body.languageTag,
    });
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "recipe.translate.requested",
      targetType: "recipe",
      targetId: params.id,
      metadata: { languageTag: body.languageTag },
    });
    return { queued: true, recipeId: params.id, languageTag: body.languageTag };
  });

  /** Lista översättningar (med verifieringsresultat) för granskning. */
  app.get("/admin/v1/recipes/:id/translations", admin, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const translations = await app.db
      .select()
      .from(schema.recipeTranslations)
      .where(eq(schema.recipeTranslations.recipeId, params.id));
    const steps = await app.db
      .select()
      .from(schema.recipeStepTranslations)
      .where(eq(schema.recipeStepTranslations.recipeId, params.id))
      .orderBy(schema.recipeStepTranslations.stepNumber);
    return {
      translations: translations.map((t) => ({
        ...t,
        steps: steps.filter((s) => s.languageTag === t.languageTag),
      })),
    };
  });

  /** Publicera/underkänn en översättning efter mänsklig granskning. */
  app.post("/admin/v1/recipes/:id/translations/:languageTag", admin, async (req) => {
    const params = z
      .object({ id: z.uuid(), languageTag: z.string().min(2).max(35) })
      .parse(req.params);
    const body = z
      .object({
        action: z.enum(["publish", "back_to_draft", "in_review"]),
        /** Redaktören kan rätta texten i samma steg. */
        title: z.string().min(1).optional(),
        description: z.string().nullable().optional(),
      })
      .parse(req.body);
    const status =
      body.action === "publish"
        ? ("published" as const)
        : body.action === "in_review"
          ? ("in_review" as const)
          : ("draft_ai" as const);
    const [row] = await app.db
      .update(schema.recipeTranslations)
      .set({
        status,
        ...(body.title ? { title: body.title, source: "human" as const } : {}),
        ...(body.description !== undefined ? { description: body.description } : {}),
        updatedAt: new Date(),
      })
      .where(
        and(
          eq(schema.recipeTranslations.recipeId, params.id),
          eq(schema.recipeTranslations.languageTag, params.languageTag),
        ),
      )
      .returning();
    if (!row) throw errors.notFound("Översättningen finns inte.");
    await audit(app.db, {
      actorUserId: req.userId,
      actorType: "admin",
      action: "recipe.translation.moderated",
      targetType: "recipe",
      targetId: params.id,
      metadata: { languageTag: params.languageTag, action: body.action },
    });
    return row;
  });
}
