/**
 * Multi-Tenant routes — tenant CRUD, member management, invites.
 * @module tenants/routes
 */

import crypto from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import {
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  BadRequestError,
  ConflictError,
} from '../../lib/errors.js';
import { getRequestProductId } from '../../lib/request-context.js';
import {
  CreateTenantSchema,
  UpdateTenantSchema,
  InviteMemberSchema,
  UpdateMemberRoleSchema,
  PLAN_LIMITS,
  type TenantDoc,
  type TenantStatus,
} from './types.js';
import * as repo from './repository.js';

function requireAuth(req: { jwtPayload?: { sub: string } }): string {
  if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
  return req.jwtPayload.sub;
}

function requireAdmin(req: { jwtPayload?: { sub: string; role?: string } }): string {
  const userId = requireAuth(req);
  if (req.jwtPayload?.role !== 'admin') throw new ForbiddenError('Admin access required');
  return userId;
}

export async function tenantRoutes(app: FastifyInstance): Promise<void> {
  // ── Create tenant ──────────────────────────────────────────
  app.post('/tenants', async (req, reply) => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const input = CreateTenantSchema.parse(req.body);

    // Check slug uniqueness
    const existing = await repo.getTenantBySlug(productId, input.slug);
    if (existing) {
      throw new ConflictError(`Tenant slug "${input.slug}" is already taken`);
    }

    const limits = PLAN_LIMITS[input.plan];
    const now = new Date().toISOString();
    const doc: TenantDoc = {
      id: `ten_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      productId,
      name: input.name,
      slug: input.slug,
      status: 'active',
      plan: input.plan,
      ownerId: userId,
      customDomain: input.customDomain ?? null,
      settings: {},
      maxMembers: limits.maxMembers,
      maxStorageBytes: limits.maxStorageBytes,
      storageUsedBytes: 0,
      createdAt: now,
      updatedAt: now,
    };

    const created = await repo.createTenant(doc);

    // Auto-add creator as owner member
    await repo.addMember({
      id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      tenantId: created.id,
      productId,
      userId,
      role: 'owner',
      displayName: req.jwtPayload?.email ?? userId,
      invitedBy: userId,
      joinedAt: now,
      updatedAt: now,
    });

    req.log.info({ tenantId: created.id, userId, slug: input.slug }, 'Tenant created');
    reply.status(201);
    return created;
  });

  // ── List tenants ───────────────────────────────────────────
  app.get('/tenants', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const {
      status,
      ownerId,
      limit: limitStr,
    } = req.query as {
      status?: string;
      ownerId?: string;
      limit?: string;
    };
    const parsedLimit = limitStr ? parseInt(limitStr, 10) : 50;
    const safeLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 200) : 50;

    return repo.listTenants(productId, {
      status: status as TenantStatus | undefined,
      ownerId,
      limit: safeLimit,
    });
  });

  // ── Get tenant ─────────────────────────────────────────────
  app.get<{ Params: { id: string } }>('/tenants/:id', async req => {
    requireAuth(req);
    const productId = getRequestProductId(req);
    const tenant = await repo.getTenant(req.params.id, productId);
    if (!tenant) throw new NotFoundError('Tenant not found');
    return tenant;
  });

  // ── Update tenant ──────────────────────────────────────────
  app.patch<{ Params: { id: string } }>('/tenants/:id', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = UpdateTenantSchema.parse(req.body);
    const updated = await repo.updateTenant(req.params.id, productId, input);
    if (!updated) throw new NotFoundError('Tenant not found');
    req.log.info({ tenantId: req.params.id }, 'Tenant updated');
    return updated;
  });

  // ── Delete tenant ──────────────────────────────────────────
  app.delete<{ Params: { id: string } }>('/tenants/:id', async (req, reply) => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const deleted = await repo.deleteTenant(req.params.id, productId);
    if (!deleted) throw new NotFoundError('Tenant not found');
    req.log.info({ tenantId: req.params.id }, 'Tenant deleted');
    reply.status(204);
    return;
  });

  // ── List members ───────────────────────────────────────────
  app.get<{ Params: { tenantId: string } }>('/tenants/:tenantId/members', async req => {
    requireAuth(req);
    const { tenantId } = req.params;
    const { limit: limitStr } = req.query as { limit?: string };
    const parsedLimit = limitStr ? parseInt(limitStr, 10) : 50;
    const safeLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 200) : 50;
    return repo.listMembers(tenantId, { limit: safeLimit });
  });

  // ── Invite member ──────────────────────────────────────────
  app.post<{ Params: { tenantId: string } }>('/tenants/:tenantId/invite', async (req, reply) => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const { tenantId } = req.params;
    const input = InviteMemberSchema.parse(req.body);

    const tenant = await repo.getTenant(tenantId, productId);
    if (!tenant) throw new NotFoundError('Tenant not found');

    // Check member limit
    const memberCount = await repo.getMemberCount(tenantId);
    if (memberCount >= tenant.maxMembers) {
      throw new BadRequestError(`Tenant has reached the maximum of ${tenant.maxMembers} members`);
    }

    // Generate invite token
    const token = crypto.randomBytes(32).toString('hex');
    const tokenHash = crypto.createHash('sha256').update(token).digest('hex');

    const now = new Date().toISOString();
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7); // 7-day expiry

    const invite = await repo.createInvite({
      id: `inv_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      tenantId,
      productId,
      email: input.email,
      role: input.role,
      invitedBy: userId,
      tokenHash,
      expiresAt: expiresAt.toISOString(),
      acceptedAt: null,
      createdAt: now,
    });

    req.log.info({ tenantId, email: input.email, inviteId: invite.id }, 'Member invited');
    reply.status(201);
    // Return token in response (would normally be emailed)
    return { invite, token };
  });

  // ── List pending invites ───────────────────────────────────
  app.get<{ Params: { tenantId: string } }>('/tenants/:tenantId/invites', async req => {
    requireAuth(req);
    const { tenantId } = req.params;
    return { invites: await repo.listPendingInvites(tenantId) };
  });

  // ── Accept invite ──────────────────────────────────────────
  app.post<{ Params: { tenantId: string } }>(
    '/tenants/:tenantId/accept-invite',
    async (req, reply) => {
      const userId = requireAuth(req);
      const productId = getRequestProductId(req);
      const { tenantId } = req.params;
      const { token } = req.body as { token?: string };

      if (!token) throw new BadRequestError('Invite token is required');

      const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
      const invite = await repo.getInviteByTokenHash(tokenHash, tenantId);
      if (!invite) throw new NotFoundError('Invalid or expired invite');

      if (new Date(invite.expiresAt) < new Date()) {
        throw new BadRequestError('Invite has expired');
      }

      // Check not already a member
      const existingMember = await repo.getMember(tenantId, userId);
      if (existingMember) {
        throw new ConflictError('Already a member of this tenant');
      }

      const now = new Date().toISOString();

      // Add member
      const member = await repo.addMember({
        id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
        tenantId,
        productId,
        userId,
        role: invite.role,
        displayName: req.jwtPayload?.email ?? userId,
        invitedBy: invite.invitedBy,
        joinedAt: now,
        updatedAt: now,
      });

      // Mark invite as accepted
      await repo.markInviteAccepted(invite.id, tenantId);

      req.log.info({ tenantId, userId, role: invite.role }, 'Invite accepted');
      reply.status(201);
      return member;
    }
  );

  // ── Update member role ─────────────────────────────────────
  app.patch<{ Params: { tenantId: string; userId: string } }>(
    '/tenants/:tenantId/members/:userId',
    async req => {
      requireAdmin(req);
      const { tenantId, userId } = req.params;
      const input = UpdateMemberRoleSchema.parse(req.body);

      const updated = await repo.updateMemberRole(tenantId, userId, input.role);
      if (!updated) throw new NotFoundError('Member not found');

      req.log.info({ tenantId, userId, role: input.role }, 'Member role updated');
      return updated;
    }
  );

  // ── Remove member ──────────────────────────────────────────
  app.delete<{ Params: { tenantId: string; memberId: string } }>(
    '/tenants/:tenantId/members/:memberId',
    async (req, reply) => {
      requireAdmin(req);
      const { tenantId, memberId } = req.params;

      const removed = await repo.removeMember(memberId, tenantId);
      if (!removed) throw new NotFoundError('Member not found');

      req.log.info({ tenantId, memberId }, 'Member removed');
      reply.status(204);
      return;
    }
  );
}
