import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { PrismaClient } from '@prisma/client';
import yaml from 'js-yaml';
import { seedTemplates } from '@mcpctl/db';
import type { SeedTemplate } from '@mcpctl/db';
import { loadConfigFromEnv } from './config/index.js';
import { createServer } from './server.js';
import { setupGracefulShutdown } from './utils/index.js';
import {
  McpServerRepository,
  SecretRepository,
  McpInstanceRepository,
  ProjectRepository,
  AuditLogRepository,
  TemplateRepository,
  RbacDefinitionRepository,
  UserRepository,
  GroupRepository,
  AuditEventRepository,
} from './repositories/index.js';
import { PromptRepository } from './repositories/prompt.repository.js';
import { PromptRequestRepository } from './repositories/prompt-request.repository.js';
import { bootstrapSystemProject } from './bootstrap/system-project.js';
import {
  McpServerService,
  SecretService,
  InstanceService,
  ProjectService,
  AuditLogService,
  DockerContainerManager,
  MetricsCollector,
  HealthAggregator,
  BackupService,
  RestoreService,
  AuthService,
  McpProxyService,
  TemplateService,
  HealthProbeRunner,
  RbacDefinitionService,
  RbacService,
  UserService,
  GroupService,
  AuditEventService,
} from './services/index.js';
import type { RbacAction } from './services/index.js';
import type { UpdateRbacDefinitionInput } from './validation/rbac-definition.schema.js';
import { createAuthMiddleware } from './middleware/auth.js';
import {
  registerMcpServerRoutes,
  registerSecretRoutes,
  registerInstanceRoutes,
  registerProjectRoutes,
  registerAuditLogRoutes,
  registerHealthMonitoringRoutes,
  registerBackupRoutes,
  registerAuthRoutes,
  registerMcpProxyRoutes,
  registerTemplateRoutes,
  registerRbacRoutes,
  registerUserRoutes,
  registerGroupRoutes,
  registerAuditEventRoutes,
} from './routes/index.js';
import { registerPromptRoutes } from './routes/prompts.js';
import { registerGitBackupRoutes } from './routes/git-backup.js';
import { PromptService } from './services/prompt.service.js';
import { GitBackupService } from './services/backup/git-backup.service.js';
import type { BackupKind } from './services/backup/yaml-serializer.js';
import { ResourceRuleRegistry } from './validation/resource-rules.js';
import { systemPromptVarsRule } from './validation/rules/system-prompt-vars.js';

type PermissionCheck =
  | { kind: 'resource'; resource: string; action: RbacAction; resourceName?: string }
  | { kind: 'operation'; operation: string }
  | { kind: 'skip' };

/**
 * Map an HTTP method + URL to a permission check.
 * Returns 'skip' for URLs that should not be RBAC-checked.
 */
function mapUrlToPermission(method: string, url: string): PermissionCheck {
  const match = url.match(/^\/api\/v1\/([a-z-]+)/);
  if (!match) return { kind: 'skip' };

  const segment = match[1] as string;

  // Operations (non-resource endpoints)
  if (segment === 'backup') return { kind: 'operation', operation: 'backup' };
  if (segment === 'restore') return { kind: 'operation', operation: 'restore' };
  if (segment === 'audit-logs' && method === 'DELETE') return { kind: 'operation', operation: 'audit-purge' };

  const resourceMap: Record<string, string | undefined> = {
    'servers': 'servers',
    'instances': 'instances',
    'secrets': 'secrets',
    'projects': 'projects',
    'templates': 'templates',
    'users': 'users',
    'groups': 'groups',
    'rbac': 'rbac',
    'audit-logs': 'rbac',
    'mcp': 'servers',
    'prompts': 'prompts',
    'promptrequests': 'promptrequests',
  };

  const resource = resourceMap[segment];
  if (resource === undefined) return { kind: 'skip' };

  // Special case: /api/v1/promptrequests/:id/approve → needs both delete+promptrequests and create+prompts
  // We check delete on promptrequests (the harder permission); create on prompts is checked in the service layer
  const approveMatch = url.match(/^\/api\/v1\/promptrequests\/([^/?]+)\/approve/);
  if (approveMatch?.[1]) {
    return { kind: 'resource', resource: 'promptrequests', action: 'delete', resourceName: approveMatch[1] };
  }

  // Special case: /api/v1/projects/:name/prompts/visible → view prompts
  const visiblePromptsMatch = url.match(/^\/api\/v1\/projects\/([^/?]+)\/prompts\/visible/);
  if (visiblePromptsMatch?.[1]) {
    return { kind: 'resource', resource: 'prompts', action: 'view' };
  }

  // Special case: /api/v1/projects/:name/promptrequests → create promptrequests
  const projectPromptrequestsMatch = url.match(/^\/api\/v1\/projects\/([^/?]+)\/promptrequests/);
  if (projectPromptrequestsMatch?.[1] && method === 'POST') {
    return { kind: 'resource', resource: 'promptrequests', action: 'create' };
  }

  // Special case: /api/v1/projects/:id/instructions → view projects
  const instructionsMatch = url.match(/^\/api\/v1\/projects\/([^/?]+)\/instructions/);
  if (instructionsMatch?.[1]) {
    return { kind: 'resource', resource: 'projects', action: 'view', resourceName: instructionsMatch[1] };
  }

  // Special case: /api/v1/projects/:id/mcp-config → requires 'expose' permission
  const mcpConfigMatch = url.match(/^\/api\/v1\/projects\/([^/?]+)\/mcp-config/);
  if (mcpConfigMatch?.[1]) {
    return { kind: 'resource', resource: 'projects', action: 'expose', resourceName: mcpConfigMatch[1] };
  }

  // Special case: /api/v1/projects/:id/servers — attach/detach requires 'edit'
  const projectServersMatch = url.match(/^\/api\/v1\/projects\/([^/?]+)\/servers/);
  if (projectServersMatch?.[1] && method !== 'GET') {
    return { kind: 'resource', resource: 'projects', action: 'edit', resourceName: projectServersMatch[1] };
  }

  // Map HTTP method to action
  let action: RbacAction;
  switch (method) {
    case 'GET':
    case 'HEAD':
      action = 'view';
      break;
    case 'POST':
      action = 'create';
      break;
    case 'DELETE':
      action = 'delete';
      break;
    default: // PUT, PATCH
      action = 'edit';
      break;
  }

  // Extract resource name/ID from URL (3rd segment: /api/v1/servers/:nameOrId)
  const nameMatch = url.match(/^\/api\/v1\/[a-z-]+\/([^/?]+)/);
  const resourceName = nameMatch?.[1];

  const check: PermissionCheck = { kind: 'resource', resource, action };
  if (resourceName !== undefined) (check as { resourceName: string }).resourceName = resourceName;
  return check;
}

/**
 * Migrate legacy 'admin' role bindings → granular roles.
 * Old format:  { role: 'admin', resource: '*' }
 * New format:  { role: 'edit', resource: '*' }, { role: 'run', resource: '*' },
 *              plus operation bindings for impersonate, logs, backup, restore, audit-purge
 */
async function migrateAdminRole(rbacRepo: InstanceType<typeof RbacDefinitionRepository>): Promise<void> {
  const definitions = await rbacRepo.findAll();
  for (const def of definitions) {
    const bindings = def.roleBindings as Array<Record<string, unknown>>;
    const hasAdminRole = bindings.some((b) => b['role'] === 'admin');
    if (!hasAdminRole) continue;

    // Replace admin bindings with granular equivalents
    const newBindings: Array<Record<string, string>> = [];
    for (const b of bindings) {
      if (b['role'] === 'admin') {
        const resource = b['resource'] as string;
        newBindings.push({ role: 'edit', resource });
        newBindings.push({ role: 'run', resource });
      } else {
        newBindings.push(b as Record<string, string>);
      }
    }
    // Add operation bindings (idempotent — only for wildcard admin)
    const hasWildcard = bindings.some((b) => b['role'] === 'admin' && b['resource'] === '*');
    if (hasWildcard) {
      const ops = ['impersonate', 'logs', 'backup', 'restore', 'audit-purge'];
      for (const op of ops) {
        if (!newBindings.some((b) => b['action'] === op)) {
          newBindings.push({ role: 'run', action: op });
        }
      }
    }

    await rbacRepo.update(def.id, { roleBindings: newBindings as UpdateRbacDefinitionInput['roleBindings'] });
    // eslint-disable-next-line no-console
    console.log(`mcpd: migrated RBAC '${def.name}' from admin → granular roles`);
  }
}

async function main(): Promise<void> {
  const config = loadConfigFromEnv();

  // Database
  const prisma = new PrismaClient({
    datasources: { db: { url: config.databaseUrl } },
  });
  await prisma.$connect();

  // Seed templates from YAML files
  const templatesDir = process.env.TEMPLATES_DIR ?? 'templates';
  const templateFiles = (() => {
    try {
      return readdirSync(templatesDir).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
    } catch {
      return [];
    }
  })();
  const templates: SeedTemplate[] = templateFiles.map((f) => {
    const content = readFileSync(join(templatesDir, f), 'utf-8');
    const parsed = yaml.load(content) as SeedTemplate;
    return {
      ...parsed,
      transport: parsed.transport ?? 'STDIO',
      version: parsed.version ?? '1.0.0',
      description: parsed.description ?? '',
      ...(parsed.healthCheck ? { healthCheck: parsed.healthCheck } : {}),
    };
  });
  await seedTemplates(prisma, templates);

  // Bootstrap system project and prompts
  await bootstrapSystemProject(prisma);

  // Repositories
  const serverRepo = new McpServerRepository(prisma);
  const secretRepo = new SecretRepository(prisma);
  const instanceRepo = new McpInstanceRepository(prisma);
  const projectRepo = new ProjectRepository(prisma);
  const auditLogRepo = new AuditLogRepository(prisma);
  const auditEventRepo = new AuditEventRepository(prisma);
  const templateRepo = new TemplateRepository(prisma);
  const rbacDefinitionRepo = new RbacDefinitionRepository(prisma);
  const userRepo = new UserRepository(prisma);
  const groupRepo = new GroupRepository(prisma);

  // CUID detection for RBAC name resolution
  const CUID_RE = /^c[^\s-]{8,}$/i;
  const nameResolvers: Record<string, { findById(id: string): Promise<{ name: string } | null> }> = {
    servers: serverRepo,
    secrets: secretRepo,
    projects: projectRepo,
    groups: groupRepo,
  };

  // Migrate legacy 'admin' role → granular roles
  await migrateAdminRole(rbacDefinitionRepo);

  // Orchestrator
  const orchestrator = new DockerContainerManager();

  // Services
  const serverService = new McpServerService(serverRepo);
  const instanceService = new InstanceService(instanceRepo, serverRepo, orchestrator, secretRepo);
  serverService.setInstanceService(instanceService);
  const secretService = new SecretService(secretRepo);
  const projectService = new ProjectService(projectRepo, serverRepo);
  const auditLogService = new AuditLogService(auditLogRepo);
  const auditEventService = new AuditEventService(auditEventRepo);
  const metricsCollector = new MetricsCollector();
  const healthAggregator = new HealthAggregator(metricsCollector, orchestrator);
  const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo);
  const restoreService = new RestoreService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo);
  const authService = new AuthService(prisma);
  const templateService = new TemplateService(templateRepo);
  const mcpProxyService = new McpProxyService(instanceRepo, serverRepo, orchestrator);
  const rbacDefinitionService = new RbacDefinitionService(rbacDefinitionRepo);
  const rbacService = new RbacService(rbacDefinitionRepo, prisma);
  const userService = new UserService(userRepo);
  const groupService = new GroupService(groupRepo, userRepo);
  const promptRepo = new PromptRepository(prisma);
  const promptRequestRepo = new PromptRequestRepository(prisma);
  const promptRuleRegistry = new ResourceRuleRegistry();
  promptRuleRegistry.register(systemPromptVarsRule);
  const promptService = new PromptService(promptRepo, promptRequestRepo, projectRepo, promptRuleRegistry);

  // Auth middleware for global hooks
  const authMiddleware = createAuthMiddleware({
    findSession: (token) => authService.findSession(token),
  });

  // Server
  const app = await createServer(config, {
    health: {
      checkDb: async () => {
        try {
          await prisma.$queryRaw`SELECT 1`;
          return true;
        } catch {
          return false;
        }
      },
    },
  });

  // ── Global auth hook ──
  // Runs on all /api/v1/* routes EXCEPT auth endpoints and health checks.
  // Tests that use createServer() directly are NOT affected — this hook
  // is only registered here in main.ts.
  app.addHook('preHandler', async (request, reply) => {
    const url = request.url;
    // Skip auth for health, auth, and root
    if (url.startsWith('/api/v1/auth/') || url === '/healthz' || url === '/health') return;
    if (!url.startsWith('/api/v1/')) return;

    // Run auth middleware
    await authMiddleware(request, reply);
  });

  // ── Global RBAC hook ──
  // Runs after the auth hook. Maps URL to resource+action and checks permissions.
  app.addHook('preHandler', async (request, reply) => {
    if (reply.sent) return; // Auth hook already rejected
    const url = request.url;
    if (url.startsWith('/api/v1/auth/') || url === '/healthz' || url === '/health') return;
    if (!url.startsWith('/api/v1/')) return;
    if (request.userId === undefined) return; // Auth hook will handle 401

    const check = mapUrlToPermission(request.method, url);
    if (check.kind === 'skip') return;

    // Extract service account identity from header (sent by mcplocal)
    const saHeader = request.headers['x-service-account'];
    const serviceAccountName = typeof saHeader === 'string' ? saHeader : undefined;

    let allowed: boolean;
    if (check.kind === 'operation') {
      allowed = await rbacService.canRunOperation(request.userId, check.operation, serviceAccountName);
    } else {
      // Resolve CUID → human name for name-scoped RBAC bindings
      if (check.resourceName !== undefined && CUID_RE.test(check.resourceName)) {
        const resolver = nameResolvers[check.resource];
        if (resolver) {
          const entity = await resolver.findById(check.resourceName);
          if (entity) check.resourceName = entity.name;
        }
      }
      allowed = await rbacService.canAccess(request.userId, check.action, check.resource, check.resourceName, serviceAccountName);
      // Compute scope for list filtering (used by preSerialization hook)
      if (allowed && check.resourceName === undefined) {
        request.rbacScope = await rbacService.getAllowedScope(request.userId, check.action, check.resource, serviceAccountName);
      }
    }
    if (!allowed) {
      return reply.code(403).send({ error: 'Forbidden' });
    }
  });

  // Routes
  registerMcpServerRoutes(app, serverService, instanceService);
  registerTemplateRoutes(app, templateService);
  registerSecretRoutes(app, secretService);
  registerInstanceRoutes(app, instanceService);
  registerProjectRoutes(app, projectService);
  registerAuditLogRoutes(app, auditLogService);
  registerAuditEventRoutes(app, auditEventService);
  registerHealthMonitoringRoutes(app, { healthAggregator, metricsCollector });
  registerBackupRoutes(app, { backupService, restoreService });
  registerAuthRoutes(app, { authService, userService, groupService, rbacDefinitionService, rbacService });
  registerMcpProxyRoutes(app, {
    mcpProxyService,
    auditLogService,
    authDeps: { findSession: (token) => authService.findSession(token) },
  });
  registerRbacRoutes(app, rbacDefinitionService);
  registerUserRoutes(app, userService);
  registerGroupRoutes(app, groupService);
  registerPromptRoutes(app, promptService, projectRepo);

  // ── Git-based backup ──
  const gitBackup = new GitBackupService(prisma);

  // Hook: enqueue backup after successful mutations (checks enabled at call time)
  const kindFromSegment: Record<string, BackupKind | undefined> = {
    servers: 'server', secrets: 'secret', projects: 'project',
    templates: 'template', users: 'user', groups: 'group',
    rbac: 'rbac', prompts: 'prompt',
  };
  app.addHook('onSend', async (request, reply, payload) => {
    if (!gitBackup.enabled) return payload;
    if (reply.statusCode >= 400) return payload;
    const method = request.method;
    if (method === 'GET' || method === 'HEAD') return payload;

    const urlMatch = request.url.match(/^\/api\/v1\/([a-z-]+)(?:\/([^/?]+))?/);
    if (!urlMatch) return payload;
    const kind = kindFromSegment[urlMatch[1]!];
    if (!kind) return payload;

    let action: 'create' | 'update' | 'delete';
    if (method === 'DELETE') action = 'delete';
    else if (method === 'POST') action = 'create';
    else action = 'update';

    // Get resource name: from URL for update/delete, from response body for create
    const nameField = kind === 'user' ? 'email' : 'name';
    let resourceName = urlMatch[2];
    if (!resourceName && typeof payload === 'string') {
      try {
        const body = JSON.parse(payload);
        resourceName = body[nameField];
      } catch { /* ignore parse errors */ }
    }
    if (!resourceName) return payload;

    const userName = request.userId ?? 'system';
    gitBackup.enqueue(kind, resourceName, action, userName).catch((err) => {
      app.log.error({ err }, `Git backup enqueue failed for ${kind}/${resourceName}`);
    });
    return payload;
  });

  // Import/delete callbacks for git restore
  const importResource = async (kind: BackupKind, _name: string, doc: Record<string, unknown>) => {
    const data = { ...doc };
    delete data.kind;
    switch (kind) {
      case 'server': await serverService.upsertByName(data); break;
      case 'secret': await secretService.upsertByName(data); break;
      case 'project': await projectService.upsertByName(data, 'system'); break;
      case 'user': await userService.upsertByEmail(data); break;
      case 'group': await groupService.upsertByName(data); break;
      case 'rbac': await rbacDefinitionService.upsertByName(data); break;
      case 'prompt': await promptService.upsertByName(data); break;
      case 'template': await templateService.upsertByName(data); break;
    }
  };
  const deleteResource = async (kind: BackupKind, name: string) => {
    switch (kind) {
      case 'server': await serverService.deleteByName(name); break;
      case 'secret': await secretService.deleteByName(name); break;
      case 'project': await projectService.deleteByName(name); break;
      case 'user': await userService.deleteByEmail(name); break;
      case 'group': await groupService.deleteByName(name); break;
      case 'rbac': await rbacDefinitionService.deleteByName(name); break;
      case 'prompt': await promptService.deleteByName(name); break;
      case 'template': await templateService.deleteByName(name); break;
    }
  };
  gitBackup.setCallbacks(importResource, deleteResource);

  // Init async — reads config from backup-ssh secret, don't block startup
  gitBackup.init().catch((err) => app.log.error({ err }, 'Git backup init failed'));

  registerGitBackupRoutes(app, gitBackup);

  // ── RBAC list filtering hook ──
  // Filters array responses to only include resources the user is allowed to see.
  app.addHook('preSerialization', async (request, _reply, payload) => {
    if (!request.rbacScope || request.rbacScope.wildcard) return payload;
    if (!Array.isArray(payload)) return payload;
    return (payload as Array<Record<string, unknown>>).filter((item) => {
      const name = item['name'];
      return typeof name === 'string' && request.rbacScope!.names.has(name);
    });
  });

  // Start
  await app.listen({ port: config.port, host: config.host });
  app.log.info(`mcpd listening on ${config.host}:${config.port}`);

  // Periodic container liveness sync — detect crashed containers
  const SYNC_INTERVAL_MS = 30_000; // 30s
  const syncTimer = setInterval(async () => {
    try {
      await instanceService.syncStatus();
    } catch (err) {
      app.log.error({ err }, 'Container status sync failed');
    }
  }, SYNC_INTERVAL_MS);

  // Health probe runner — periodic MCP tool-call probes (like k8s livenessProbe)
  const healthProbeRunner = new HealthProbeRunner(
    instanceRepo,
    serverRepo,
    orchestrator,
    { info: (msg) => app.log.info(msg), error: (obj, msg) => app.log.error(obj, msg) },
  );
  healthProbeRunner.start(15_000);

  // Graceful shutdown
  setupGracefulShutdown(app, {
    disconnectDb: async () => {
      clearInterval(syncTimer);
      healthProbeRunner.stop();
      gitBackup.stop();
      await prisma.$disconnect();
    },
  });
}

main().catch((err) => {
  console.error('Failed to start mcpd:', err);
  process.exit(1);
});
