/**
 * Composition root. The only place allowed to wire modules together.
 * Cross-cutting HTTP behavior (request id, logging, errors) is explicit here.
 */
import { randomUUID } from 'node:crypto';
import { performance } from 'node:perf_hooks';
import Fastify, { type FastifyInstance } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import fastifyCors from '@fastify/cors';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
import type { IncomingMessage } from 'node:http';
import type pg from 'pg';
import { swaggerConfig } from '../shared/swagger.js';
import { registerHealthRoutes } from '../modules/health/index.js';
import {
  createSessionAuthenticator,
  registerIdentityRoutes,
  SettingsPasswordResetMailer,
  SettingsWelcomeMailer,
} from '../modules/identity/index.js';
import {
  registerBackofficeRoutes,
  createBackofficeSessionAuthenticator,
  createCombinedAuthenticator,
} from '../modules/backoffice/index.js';
import { registerUsersRoutes } from '../modules/users/index.js';
import { registerCategoriesRoutes } from '../modules/categories/index.js';
import { registerCatalogRoutes } from '../modules/catalog/index.js';
import { registerBrandsRoutes } from '../modules/brands/index.js';
import { createInventoryService, registerInventoryRoutes } from '../modules/inventory/index.js';
import { createPricingService, registerPricingRoutes } from '../modules/pricing/index.js';
import { createPromotionService, registerPromotionsRoutes } from '../modules/promotions/index.js';
import { registerCartRoutes } from '../modules/cart/index.js';
import { registerShippingRoutes } from '../modules/shipping/index.js';
import { registerOrdersRoutes } from '../modules/orders/index.js';
import { PgStoreRepository } from '../modules/pos/infrastructure/pg-store-repository.js';
import { PgTerminalRepository } from '../modules/pos/infrastructure/pg-terminal-repository.js';
import { PgPaymentMethodRepository } from '../modules/pos/infrastructure/pg-payment-method-repository.js';
import { PgCashSessionRepository } from '../modules/pos/infrastructure/pg-cash-session-repository.js';
import { registerCheckoutRoutes } from '../modules/checkout/index.js';
import { registerPaymentsRoutes } from '../modules/payments/index.js';
import { registerNotificationsRoutes } from '../modules/notifications/index.js';
import { registerReportingRoutes } from '../modules/reporting/index.js';
import { registerPosRoutes } from '../modules/pos/api/pos.routes.js';
import { registerReviewsRoutes } from '../modules/reviews/index.js';
import { registerCmsRoutes } from '../modules/cms/index.js';
import { registerStoreSettingsRoutes } from '../modules/store-settings/index.js';
import { CacheService, InMemoryCacheAdapter, registerCacheRoutes } from '../modules/cache/index.js';
import { AuditLogger, RateLimiter, registerSecurityRoutes } from '../modules/security/index.js';
import { getLogBroadcaster } from '../infrastructure/logging/log-broadcaster.js';
import { registerAdminStatsRoutes } from '../modules/admin-stats/index.js';
import { createInMemoryTelemetry, registerMetricsRoutes } from '../modules/observability/index.js';
import { LoggingEmailProvider } from '../modules/notifications/index.js';
import { createFlagStore, type FeatureFlagProvider } from '../modules/flags/index.js';
import { AppError, errorEnvelope } from '../shared/errors.js';
import { createLogger, type Logger } from '../infrastructure/logging/logger.js';

declare module 'fastify' {
  interface FastifyInstance {
    flags: FeatureFlagProvider;
  }
}

const REQUEST_ID_HEADER = 'x-request-id';
const SAFE_REQUEST_ID = /^[A-Za-z0-9._-]{1,128}$/;

export interface BuildAppDeps {
  /** Injectable logger so tests can capture structured output. */
  logger?: Logger;
  /** Feature flags. Default: empty store, every flag OFF (fail-safe). */
  flags?: FeatureFlagProvider;
  /** Database pool. When present, DB-backed modules (identity) are wired. */
  pool?: pg.Pool;
  /** Secure cookie flag forwarded to identity routes. */
  cookieSecure?: boolean;
}

function generateRequestId(raw: IncomingMessage): string {
  const incoming = raw.headers[REQUEST_ID_HEADER];
  const value = Array.isArray(incoming) ? incoming[0] : incoming;
  if (typeof value === 'string' && SAFE_REQUEST_ID.test(value)) {
    return value;
  }
  return randomUUID();
}

/**
 * Composition root. The only place allowed to wire modules together.
 */
export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance> {
  const logger = deps.logger ?? createLogger();
  const flags = deps.flags ?? createFlagStore();
  const startTimes = new WeakMap<FastifyRequest, number>();

  const app = Fastify({ logger: false, genReqId: generateRequestId });
  // Route schemas are also the OpenAPI source. Several endpoints intentionally
  // use broad response schemas, so serialize the explicit route DTOs without
  // letting fast-json-stringify discard fields absent from those broad schemas.
  app.setSerializerCompiler(() => (payload) => JSON.stringify(payload));
  app.decorate('flags', flags);

  await app.register(fastifyCors, {
    origin: true,
    credentials: true,
  });

  app.addHook('onRequest', async (request, reply) => {
    startTimes.set(request, performance.now());
    void reply.header(REQUEST_ID_HEADER, request.id);
  });

  app.addHook('onResponse', async (request, reply) => {
    const startedAt = startTimes.get(request);
    const durationMs =
      startedAt === undefined ? 0 : Math.round((performance.now() - startedAt) * 100) / 100;
    logger.info(
      {
        requestId: request.id,
        method: request.method,
        url: request.url,
        statusCode: reply.statusCode,
        durationMs,
      },
      'request completed',
    );
  });

  app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
    const requestId = request.id;
    let statusCode =
      error.statusCode !== undefined && error.statusCode >= 400 && error.statusCode < 600
        ? error.statusCode
        : 500;
    let code = 'INTERNAL_ERROR';
    let message = 'Internal Server Error';
    let details: AppError['details'];

    if (error instanceof AppError) {
      statusCode = error.statusCode;
      code = error.code;
      details = error.details;
      message = statusCode < 500 ? error.message : 'Internal Server Error';
    } else if (statusCode < 500) {
      if (error.code === 'FST_ERR_VALIDATION') {
        code = 'VALIDATION_ERROR';
        message = 'Invalid request';
      } else {
        code = error.code ?? 'BAD_REQUEST';
        message = error.message;
      }
    }

    if (statusCode >= 500) {
      // Stack stays server-side: logs only, tagged with the request id.
      logger.error({ err: error, requestId }, 'request failed');
    } else {
      logger.info({ requestId, statusCode, code }, 'request rejected');
    }

    void reply.code(statusCode).send(errorEnvelope(statusCode, code, message, requestId, details));
  });

  app.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
    void reply.code(404).send(errorEnvelope(404, 'NOT_FOUND', 'Not Found', request.id));
  });

  await app.register(async (instance) => {
    await registerHealthRoutes(instance);
  });

  // Cookie infrastructure is cross-module (identity + users): register once at root.
  await app.register(fastifyCookie);

  // ── OpenAPI / Swagger ────────────────────────────────────────────────────────
  await app.register(fastifySwagger, swaggerConfig);
  await app.register(fastifySwaggerUi, {
    routePrefix: '/docs',
    uiConfig: {
      docExpansion: 'list',
      deepLinking: true,
      tryItOutEnabled: true,
    },
  });

  if (deps.pool) {
    // Session resolution; created before identity registration so it can be passed
    // to identity's /auth/me route.
    const authenticate = createSessionAuthenticator(deps.pool);
    const auditLogger = new AuditLogger(deps.pool as pg.Pool);

    await app.register(async (instance) => {
      await registerIdentityRoutes(instance, {
        pool: deps.pool as pg.Pool,
        cookieSecure: deps.cookieSecure,
        authenticate,
        welcomeMailer: new SettingsWelcomeMailer(deps.pool as pg.Pool),
        passwordReset: {
          mailer: new SettingsPasswordResetMailer(deps.pool as pg.Pool),
          audit: (entry) => {
            // Best-effort audit; never block the request on audit failures.
            void auditLogger
              .log({
                actorId: entry.userId ?? null,
                action: entry.action,
                target: entry.email ?? entry.userId ?? 'unknown',
                metadata: { ip: entry.ip ?? null },
              })
              .catch(() => undefined);
          },
        },
      });
    });

    // FIX-14: backoffice has its own auth mechanism (backoffice_session cookie,
    // backoffice_users table). Two more authenticators:
    //   backofficeAuth — backoffice_session only (for backoffice routes).
    //   combinedAuth   — tries backoffice_session first, then mdv_session
    //                    (for routes shared by both admin and storefront).
    const backofficeAuth = createBackofficeSessionAuthenticator(deps.pool as pg.Pool);
    const combinedAuth = createCombinedAuthenticator(deps.pool as pg.Pool);

    await app.register(async (instance) => {
      await registerBackofficeRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: backofficeAuth,
      });
    });

    // Reporting contracts + RBAC foundation (F-143). Reporting is backoffice-only
    // sharing the combined authenticator; F-146 adds ReportingService reads.
    await app.register(async (instance) => {
      await registerReportingRoutes(instance, {
        authenticate: combinedAuth,
        pool: deps.pool as pg.Pool,
      });
    });

    // Session resolution is identity's; users receives it by injection so no
    // module ever imports another module.
    await app.register(async (instance) => {
      await registerUsersRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerCategoriesRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerBrandsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    const pricing = createPricingService(deps.pool);
    await app.register(async (instance) => {
      await registerCatalogRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
        logger,
        pricing,
      });
    });

    const inventory = createInventoryService(deps.pool);
    await app.register(async (instance) => {
      await registerInventoryRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerPricingRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    const promotions = createPromotionService(deps.pool);
    await app.register(async (instance) => {
      await registerPromotionsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerCartRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
        pricing,
        inventory,
        promotions,
      });
    });

    await app.register(async (instance) => {
      await registerShippingRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    // POS module repositories (wired here so POS-004+ can use them)
    const posStoreRepo = deps.pool ? new PgStoreRepository(deps.pool) : null;
    const posTerminalRepo = deps.pool ? new PgTerminalRepository(deps.pool) : null;
    const posPaymentMethodRepo = deps.pool ? new PgPaymentMethodRepository(deps.pool) : null;
    const posSessionRepo = deps.pool ? new PgCashSessionRepository(deps.pool) : null;

    await app.register(async (instance) => {
      await registerOrdersRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    // POS routes (POS-004)
    if (deps.pool && combinedAuth) {
      await app.register(async (instance) => {
        await registerPosRoutes(instance, {
          pool: deps.pool as pg.Pool,
          authenticate: combinedAuth,
        });
      });
    }

    const { telemetry, meter } = createInMemoryTelemetry();

    await app.register(async (instance) => {
      await registerMetricsRoutes(instance, { meter, authenticate: combinedAuth });
    });

    await app.register(async (instance) => {
      await registerCheckoutRoutes(instance, {
        pool: deps.pool as pg.Pool,
        // Checkout creates customer-owned orders. Do not use combinedAuth here:
        // a coexisting backoffice cookie must never override the storefront user.
        authenticate,
        tracer: telemetry.tracer,
      });
    });

    await app.register(async (instance) => {
      await registerPaymentsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
        stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? 'whsec_test',
        stripeSecretKey: process.env.STRIPE_SECRET_KEY,
      });
    });

    await app.register(async (instance) => {
      await registerNotificationsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
        emailProvider: new LoggingEmailProvider(),
      });
    });

    await app.register(async (instance) => {
      await registerReviewsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerCmsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    await app.register(async (instance) => {
      await registerStoreSettingsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
      await registerAdminStatsRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
      });
    });

    const cache = new CacheService(new InMemoryCacheAdapter());
    cache.registerContract({
      name: 'product',
      keyPattern: 'product:{slug}',
      ttlSeconds: 300,
      sourceOfTruth: 'catalog_products',
      invalidation: 'ProductUpdated',
    });
    cache.registerContract({
      name: 'category',
      keyPattern: 'category:{slug}',
      ttlSeconds: 300,
      sourceOfTruth: 'categories_categories',
      invalidation: 'CategoryUpdated',
    });
    cache.registerContract({
      name: 'nav',
      keyPattern: 'nav:tree',
      ttlSeconds: 600,
      sourceOfTruth: 'categories_categories',
      invalidation: 'NavigationInvalidated',
    });
    cache.registerContract({
      name: 'search_popular',
      keyPattern: 'search:popular',
      ttlSeconds: 60,
      sourceOfTruth: 'catalog_search',
      invalidation: 'SearchInvalidated',
    });
    await app.register(async (instance) => {
      await registerCacheRoutes(instance, { cache, authenticate: combinedAuth });
    });

    const rateLimiter = new RateLimiter();
    await app.register(async (instance) => {
      const broadcaster = getLogBroadcaster();
      await registerSecurityRoutes(instance, {
        pool: deps.pool as pg.Pool,
        authenticate: combinedAuth,
        rateLimiter,
        auditLogger,
        broadcaster,
      });
    });
  }

  return app;
}
