import type { Request, Response, NextFunction } from 'express';
import type { Pool } from 'pg';
import { prisma, tenantDb } from '../config/database.js';
import { isGlobalAdmin } from '../utils/global-admin.js';
import { decryptAesGcm, deriveAesKey } from '@horux/core';
import { env } from '../config/env.js';

declare global {
  namespace Express {
    interface Request {
      tenantPool?: Pool;
      viewingTenantId?: string;
    }
  }
}

// Cache: tenantId -> { databaseName, expires }
// Only used for MANAGED tenants. BYO tenants always query Prisma so connection info stays fresh.
const tenantDbCache = new Map<string, { databaseName: string; expires: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

type ConnectionOverride = { host: string; port: number; user: string; password: string };

/**
 * Decrypt the BYO connection string stored in the central DB.
 * Returns a connection override object, or null if decryption fails.
 */
async function resolveBYOConnection(
  dbConnectionEnc: string,
  dbConnectionIv: string,
  tenantId: string,
): Promise<ConnectionOverride | null> {
  try {
    const encKey = env.CONNECTOR_ENCRYPTION_KEY
      ? deriveAesKey(env.CONNECTOR_ENCRYPTION_KEY)
      : deriveAesKey(env.FIEL_ENCRYPTION_KEY);

    const encData = Buffer.from(dbConnectionEnc, 'base64');
    const iv = Buffer.from(dbConnectionIv, 'base64');
    // AES-GCM: last 16 bytes of the ciphertext blob are the auth tag
    const authTag = encData.subarray(encData.length - 16);
    const ciphertext = encData.subarray(0, encData.length - 16);

    const decrypted = decryptAesGcm(ciphertext, iv, authTag, encKey);
    const config = JSON.parse(decrypted.toString('utf-8'));

    return {
      host: config.host,
      port: config.port ?? 5432,
      user: config.user,
      password: config.password,
    };
  } catch (err) {
    console.error(`[TenantMiddleware] BYO decrypt failed for tenant ${tenantId}:`, err);
    return null;
  }
}

export function invalidateTenantDbCache(tenantId: string) {
  tenantDbCache.delete(tenantId);
}

export async function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.user) {
      return res.status(401).json({ message: 'No autenticado' });
    }

    let tenantId = req.user.tenantId;

    // Admin impersonation via X-View-Tenant header (global admin only)
    const viewTenantHeader = req.headers['x-view-tenant'] as string;
    if (viewTenantHeader) {
      const globalAdmin = await isGlobalAdmin(req.user.tenantId, req.user.role);
      if (!globalAdmin) {
        return res.status(403).json({ message: 'No autorizado para ver otros tenants' });
      }

      const viewedTenant = await prisma.tenant.findFirst({
        where: {
          OR: [
            { id: viewTenantHeader },
            { rfc: viewTenantHeader },
          ],
        },
        select: { id: true, databaseName: true, active: true, dbMode: true, dbConnectionEnc: true, dbConnectionIv: true },
      });

      if (!viewedTenant) {
        return res.status(404).json({ message: 'Tenant no encontrado' });
      }

      if (!viewedTenant.active) {
        return res.status(403).json({ message: 'Tenant inactivo' });
      }

      tenantId = viewedTenant.id;
      req.viewingTenantId = viewedTenant.id;

      let impersonateOverride: ConnectionOverride | undefined;
      if (viewedTenant.dbMode === 'BYO' && viewedTenant.dbConnectionEnc && viewedTenant.dbConnectionIv) {
        const override = await resolveBYOConnection(viewedTenant.dbConnectionEnc, viewedTenant.dbConnectionIv, tenantId);
        if (!override) {
          return res.status(503).json({ message: 'Base de datos del despacho no disponible. Verifica tu connector.' });
        }
        impersonateOverride = override;
      }

      req.tenantPool = await tenantDb.getPool(tenantId, viewedTenant.databaseName, impersonateOverride);
      return next();
    }

    // Normal flow: query tenant details including BYO fields.
    // BYO tenants bypass the databaseName cache so connection info stays fresh.
    // The 5-min idle eviction in TenantConnectionManager still protects against excessive pool creation.
    const tenant = await prisma.tenant.findUnique({
      where: { id: tenantId },
      select: { databaseName: true, dbMode: true, dbConnectionEnc: true, dbConnectionIv: true },
    });

    if (!tenant?.databaseName) {
      return res.status(404).json({ message: 'Tenant no encontrado' });
    }

    let connectionOverride: ConnectionOverride | undefined;

    if (tenant.dbMode === 'BYO' && tenant.dbConnectionEnc && tenant.dbConnectionIv) {
      const override = await resolveBYOConnection(tenant.dbConnectionEnc, tenant.dbConnectionIv, tenantId);
      if (!override) {
        return res.status(503).json({ message: 'Base de datos del despacho no disponible. Verifica tu connector.' });
      }
      connectionOverride = override;
    } else {
      // MANAGED: keep the databaseName cache warm to avoid repeated Prisma queries
      tenantDbCache.set(tenantId, { databaseName: tenant.databaseName, expires: Date.now() + CACHE_TTL });
    }

    req.tenantPool = await tenantDb.getPool(tenantId, tenant.databaseName, connectionOverride);
    next();
  } catch (error) {
    console.error('[TenantMiddleware] Error:', error);
    return res.status(500).json({ message: 'Error al resolver tenant' });
  }
}
