import {
  auditLog,
  builds,
  createDb,
  desc,
  eq,
  inArray,
  mcpServers,
  memberships,
  organizations,
  supportMessages,
  supportTickets,
  users,
} from '@bmm/db';
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { audit } from '../lib/audit.js';
import { stopContainer } from '../lib/docker.js';
import { requireAuth } from '../plugins/session.js';

const SESSION_COOKIE = 'bmm_session';

const db = createDb();

export async function accountRoutes(app: FastifyInstance): Promise<void> {
  // ─── Profile: read + update ───────────────────────────────────────────
  app.get('/v1/account/profile', { preHandler: requireAuth }, async (req, reply) => {
    const user = req.user!;
    const [row] = await db
      .select({
        id: users.id,
        email: users.email,
        name: users.name,
        phone: users.phone,
        isAdmin: users.isAdmin,
        createdAt: users.createdAt,
      })
      .from(users)
      .where(eq(users.id, user.userId))
      .limit(1);
    if (!row) return reply.code(404).send({ error: 'user_not_found' });
    return reply.send({ profile: row });
  });

  app.patch('/v1/account/profile', { preHandler: requireAuth }, async (req, reply) => {
    const user = req.user!;
    const Body = z.object({
      name: z.string().min(1).max(128).optional(),
    });
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });
    if (!parsed.data.name) return reply.send({ ok: true, changed: false });

    await db
      .update(users)
      .set({ name: parsed.data.name })
      .where(eq(users.id, user.userId));

    await audit({
      orgId: user.orgId,
      userId: user.userId,
      action: 'account.profile_updated',
      resourceType: 'user',
      ipAddress: req.ip,
    });

    return reply.send({ ok: true, changed: true });
  });

  /**
   * GDPR Art. 15 / Swiss DSG Art. 25 — right of access. Returns every record
   * we hold that belongs to the calling user. Excludes hashed passwords,
   * encrypted secret payloads, and any other user's data. Streamed as JSON
   * attachment so the browser downloads it directly.
   */
  app.get('/v1/account/export', { preHandler: requireAuth }, async (req, reply) => {
    const user = req.user!;

    const [userRow] = await db.select().from(users).where(eq(users.id, user.userId)).limit(1);
    const [org] = await db
      .select()
      .from(organizations)
      .where(eq(organizations.id, user.orgId))
      .limit(1);
    const orgServers = await db
      .select()
      .from(mcpServers)
      .where(eq(mcpServers.orgId, user.orgId));
    const serverIds = orgServers.map((s) => s.id);
    const orgBuilds =
      serverIds.length > 0
        ? await db.select().from(builds).where(inArray(builds.serverId, serverIds))
        : [];
    const userAudit = await db
      .select()
      .from(auditLog)
      .where(eq(auditLog.userId, user.userId))
      .orderBy(desc(auditLog.createdAt))
      .limit(1000);
    const userTickets = await db
      .select()
      .from(supportTickets)
      .where(eq(supportTickets.userId, user.userId));
    const ticketIds = userTickets.map((t) => t.id);
    const userTicketMessages =
      ticketIds.length > 0
        ? await db
            .select()
            .from(supportMessages)
            .where(inArray(supportMessages.ticketId, ticketIds))
        : [];

    await audit({
      orgId: user.orgId,
      userId: user.userId,
      action: 'account.export',
      resourceType: 'account',
      ipAddress: req.ip,
    });

    reply
      .header('Content-Type', 'application/json; charset=utf-8')
      .header(
        'Content-Disposition',
        `attachment; filename="buildmymcpserver-export-${Date.now()}.json"`,
      );

    return reply.send({
      exportedAt: new Date().toISOString(),
      _format: 'BuildMyMCPServer Account Export v1',
      _excluded: [
        'password hashes',
        'encrypted secret payloads',
        'session tokens',
        'other users in the same organization',
      ],
      user: userRow
        ? {
            id: userRow.id,
            email: userRow.email,
            name: userRow.name,
            phone: userRow.phone,
            isAdmin: userRow.isAdmin,
            createdAt: userRow.createdAt,
          }
        : null,
      organization: org
        ? {
            id: org.id,
            slug: org.slug,
            name: org.name,
            plan: org.plan,
            createdAt: org.createdAt,
          }
        : null,
      servers: orgServers.map((s) => ({
        id: s.id,
        slug: s.slug,
        name: s.name,
        status: s.status,
        publicUrl: s.publicUrl,
        toolsSchema: s.toolsSchema,
        createdAt: s.createdAt,
      })),
      builds: orgBuilds.map((b) => ({
        id: b.id,
        serverId: b.serverId,
        version: b.version,
        prompt: b.prompt,
        status: b.status,
        createdAt: b.createdAt,
      })),
      audit: userAudit.map((a) => ({
        id: a.id,
        action: a.action,
        resourceType: a.resourceType,
        resourceId: a.resourceId,
        metadata: a.metadata,
        ipAddress: a.ipAddress,
        createdAt: a.createdAt,
      })),
      supportTickets: userTickets,
      supportMessages: userTicketMessages,
    });
  });

  /**
   * GDPR Art. 17 / Swiss DSG Art. 32 — right to erasure. Self-service account
   * deletion. Requires the caller to re-type their email (or phone) as a
   * confirmation guard. For every org where the caller is the SOLE member, we
   * stop its running containers and hard-delete the org (FK cascade removes its
   * servers, builds, logs and encrypted secrets). Orgs with other members are
   * left intact — only the caller's membership goes. Finally the user row is
   * deleted (cascade drops sessions; audit/ticket/template refs are set null).
   */
  app.delete('/v1/account', { preHandler: requireAuth }, async (req, reply) => {
    const user = req.user!;
    const Body = z.object({ confirm: z.string().min(1) });
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });

    const [row] = await db
      .select({ email: users.email, phone: users.phone })
      .from(users)
      .where(eq(users.id, user.userId))
      .limit(1);
    if (!row) return reply.code(404).send({ error: 'user_not_found' });

    // Confirmation: must match the account's own email or phone.
    const expected = (row.email ?? row.phone ?? '').trim().toLowerCase();
    if (!expected || parsed.data.confirm.trim().toLowerCase() !== expected) {
      return reply.code(400).send({
        error: 'confirm_mismatch',
        detail: 'Type your account email (or phone) exactly to confirm deletion.',
      });
    }

    const memberRows = await db
      .select({ orgId: memberships.orgId })
      .from(memberships)
      .where(eq(memberships.userId, user.userId));
    const orgIds = [...new Set(memberRows.map((m) => m.orgId))];
    const deletedOrgIds: string[] = [];

    for (const orgId of orgIds) {
      const members = await db
        .select({ userId: memberships.userId })
        .from(memberships)
        .where(eq(memberships.orgId, orgId));
      // Only erase the org if this user is its sole member — never nuke a
      // teammate's data. (Multi-member orgs: just the membership is dropped
      // when the user row is deleted below.)
      if (members.length > 1) continue;

      // Stop live containers before the cascade removes their DB rows.
      const servers = await db
        .select({ containerId: mcpServers.containerId, slug: mcpServers.slug })
        .from(mcpServers)
        .where(eq(mcpServers.orgId, orgId));
      for (const s of servers) {
        if (s.containerId) {
          try {
            await stopContainer(s.containerId, s.slug ?? undefined);
          } catch {
            // best-effort — a leftover container must not block erasure
          }
        }
      }
      await db.delete(organizations).where(eq(organizations.id, orgId));
      deletedOrgIds.push(orgId);
    }

    // Audit while userId is still valid (the row survives erasure; its userId
    // is set null by cascade). No orgId — those rows are already gone.
    await audit({
      userId: user.userId,
      action: 'account.deleted',
      resourceType: 'account',
      metadata: { deletedOrgIds, email: row.email ?? null },
      ipAddress: req.ip,
    });

    // Delete the user — cascade removes sessions; nulls audit/ticket/template refs.
    await db.delete(users).where(eq(users.id, user.userId));

    reply.clearCookie(SESSION_COOKIE, { path: '/' });
    return reply.send({ ok: true, deletedOrgIds });
  });
}
