/**
 * Finance Service
 * Handles payments, ledgers, and BTC jewelry-box workflows.
 */

import { fileURLToPath } from 'url';
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import {
  AppError,
  errorHandler,
  createLogger,
  registerSecurityPlugins,
  addCorrelationId,
  addRequestLogging,
  getEnv,
  authenticateJWT,
  requireRole,
} from '@the-order/shared';
import {
  type BasketMandate,
  type CreateBasketMandate,
  type CreateBtcDeposit,
  type CreateBridgeWithdrawal,
} from '@the-order/schemas';
import {
  healthCheck as dbHealthCheck,
  getPool,
  createLedgerEntry,
  createPayment,
  updatePaymentStatus,
} from '@the-order/database';
import { StripePaymentGateway } from '@the-order/payment-gateway';
import {
  JewelryBoxStore,
  createDefaultBridgeWithdrawal,
} from './jewelry-box-store';

const logger = createLogger('finance-service');
const env = getEnv();
const PRIVILEGED_FINANCE_ROLES = new Set(['admin', 'accountant', 'finance']);

export type CreateServerOptions = {
  jewelryBoxStore?: JewelryBoxStore;
  disableSwagger?: boolean;
};

function isPrivilegedFinanceUser(request: FastifyRequest): boolean {
  return (request.user?.roles ?? []).some((role) => PRIVILEGED_FINANCE_ROLES.has(role));
}

function assertClientAccess(request: FastifyRequest, clientId: string): void {
  if (!request.user) {
    throw new AppError(401, 'UNAUTHORIZED', 'Authentication required');
  }

  if (request.user.id === clientId || isPrivilegedFinanceUser(request)) {
    return;
  }

  throw new AppError(403, 'FORBIDDEN', 'Cannot access another client\'s finance basket');
}

function resolveClientScope(request: FastifyRequest, clientId?: string): string | undefined {
  if (clientId) {
    assertClientAccess(request, clientId);
    return clientId;
  }

  return isPrivilegedFinanceUser(request) ? undefined : request.user?.id;
}

function getAccessibleBasket(
  request: FastifyRequest,
  store: JewelryBoxStore,
  basketId: string,
): BasketMandate {
  const basket = store.getBasket(basketId);
  if (!basket) {
    throw new AppError(404, 'NOT_FOUND', 'Basket not found');
  }

  assertClientAccess(request, basket.clientId);
  return basket;
}

function createPaymentGateway(): StripePaymentGateway | null {
  try {
    return env.PAYMENT_GATEWAY_API_KEY ? new StripePaymentGateway() : null;
  } catch (error) {
    logger.warn({ err: error }, 'Payment gateway not configured');
    return null;
  }
}

async function safeDbHealthCheck(): Promise<boolean> {
  try {
    return await dbHealthCheck();
  } catch (error) {
    logger.warn({ err: error }, 'Database health check failed');
    return false;
  }
}

async function registerDocs(server: any, disableSwagger = false): Promise<void> {
  if (disableSwagger) {
    return;
  }

  const swaggerUrl =
    env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4003' : undefined);

  if (!swaggerUrl) {
    logger.warn('SWAGGER_SERVER_URL not set, Swagger documentation will not be available');
    return;
  }

  await server.register(fastifySwagger, {
    openapi: {
      info: {
        title: 'Finance Service API',
        description: 'Payments, ledgers, and BTC jewelry-box workflows',
        version: '1.0.0',
      },
      servers: [
        {
          url: swaggerUrl,
          description: env.NODE_ENV || 'Development server',
        },
      ],
    },
  });

  await server.register(fastifySwaggerUI, {
    routePrefix: '/docs',
  });
}

function registerHealthRoute(server: any): void {
  server.get(
    '/health',
    {
      schema: {
        description: 'Health check endpoint',
        tags: ['health'],
        response: {
          200: {
            type: 'object',
            properties: {
              status: { type: 'string' },
              service: { type: 'string' },
              database: { type: 'string' },
            },
          },
        },
      },
    },
    async () => {
      const dbHealthy = await safeDbHealthCheck();
      return {
        status: dbHealthy ? 'ok' : 'degraded',
        service: 'finance',
        database: dbHealthy ? 'connected' : 'disconnected',
      };
    },
  );
}

function registerLedgerRoute(server: any): void {
  server.post(
    '/ledger/entry',
    {
      preHandler: [authenticateJWT as never, requireRole('admin', 'accountant', 'finance') as never],
      schema: {
        description: 'Create a ledger entry',
        tags: ['ledger'],
        body: {
          type: 'object',
          additionalProperties: false,
          required: ['accountId', 'type', 'amount', 'currency'],
          properties: {
            accountId: { type: 'string' },
            type: { type: 'string', enum: ['debit', 'credit'] },
            amount: { type: 'number', exclusiveMinimum: 0 },
            currency: { type: 'string', minLength: 3, maxLength: 3 },
            description: { type: 'string' },
            reference: { type: 'string' },
          },
        },
      },
    },
    async (
      request: FastifyRequest<{
        Body: {
          accountId: string;
          type: 'debit' | 'credit';
          amount: number;
          currency: string;
          description?: string;
          reference?: string;
        };
      }>,
      reply: FastifyReply,
    ) => {
      const body = request.body;
      const entry = await createLedgerEntry({
        account_id: body.accountId,
        type: body.type,
        amount: body.amount,
        currency: body.currency,
        description: body.description,
        reference: body.reference,
      });

      return reply.status(201).send({ entry });
    },
  );
}

function registerPaymentsRoute(server: any, paymentGateway: StripePaymentGateway | null): void {
  const handler = async (
    request: FastifyRequest<{
      Body: {
        amount: number;
        currency: string;
        paymentMethod: string;
        metadata?: Record<string, string>;
      };
    }>,
    reply: FastifyReply,
  ) => {
    const body = request.body;
    const payment = await createPayment({
      amount: body.amount,
      currency: body.currency,
      status: 'pending',
      payment_method: body.paymentMethod,
    });

    if (!paymentGateway) {
      return reply.status(201).send({ payment });
    }

    try {
      const result = await paymentGateway.processPayment(
        body.amount,
        body.currency,
        body.paymentMethod,
        {
          payment_id: payment.id,
          ...body.metadata,
        },
      );

      const updatedPayment = await updatePaymentStatus(
        payment.id,
        result.status,
        result.transactionId,
        result.gatewayResponse,
      );

      return reply.status(201).send({ payment: updatedPayment });
    } catch (error) {
      logger.error({ err: error, paymentId: payment.id }, 'Payment processing failed');
      await updatePaymentStatus(payment.id, 'failed', undefined, { error: String(error) });
      throw error;
    }
  };

  const options = {
    preHandler: [authenticateJWT as never],
    schema: {
      description: 'Process a payment',
      tags: ['payments'],
      body: {
        type: 'object',
        additionalProperties: false,
        required: ['amount', 'currency', 'paymentMethod'],
        properties: {
          amount: { type: 'number', exclusiveMinimum: 0 },
          currency: { type: 'string', minLength: 3, maxLength: 3 },
          paymentMethod: { type: 'string' },
          description: { type: 'string' },
          metadata: {
            type: 'object',
            additionalProperties: { type: 'string' },
          },
        },
      },
    },
  };

  server.post('/payments', options, handler);
  server.post('/api/v1/payments', options, handler);
}

function registerJewelryBoxRoutes(server: any, store: JewelryBoxStore): void {
  server.post(
    '/api/v1/baskets',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'Create a Chain 138 basket mandate for BTC settlement flows',
        tags: ['jewelry-box'],
        body: {
          type: 'object',
          additionalProperties: false,
          required: ['clientId', 'mandateName', 'chain138VaultAddress', 'allocations'],
          properties: {
            clientId: { type: 'string', minLength: 1 },
            mandateName: { type: 'string', minLength: 1 },
            chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
            baseAssetSymbol: { type: 'string', minLength: 2, maxLength: 16 },
            allocations: {
              type: 'array',
              minItems: 1,
              items: {
                type: 'object',
                additionalProperties: false,
                required: ['symbol', 'targetWeightBps'],
                properties: {
                  symbol: { type: 'string', minLength: 2, maxLength: 16 },
                  targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 },
                  routeHint: { type: 'string' },
                },
              },
            },
          },
        },
      },
    },
    async (
      request: FastifyRequest<{ Body: CreateBasketMandate }>,
      reply: FastifyReply,
    ) => {
      assertClientAccess(request, request.body.clientId);
      const basket = store.createBasket(request.body);
      return reply.status(201).send({ basket });
    },
  );

  server.post(
    '/api/v1/btc-deposits',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'Create a BTC deposit instruction and associate it with a jewelry-box basket',
        tags: ['jewelry-box'],
        body: {
          type: 'object',
          additionalProperties: false,
          required: ['clientId', 'chain138VaultAddress'],
          properties: {
            clientId: { type: 'string', minLength: 1 },
            basketId: { type: 'string' },
            mandateName: { type: 'string', minLength: 1 },
            chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
            expectedAmountSats: { type: 'integer', minimum: 1 },
            clientReference: { type: 'string', minLength: 1 },
            allocations: {
              type: 'array',
              minItems: 1,
              items: {
                type: 'object',
                additionalProperties: false,
                required: ['symbol', 'targetWeightBps'],
                properties: {
                  symbol: { type: 'string', minLength: 2, maxLength: 16 },
                  targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 },
                  routeHint: { type: 'string' },
                },
              },
            },
          },
        },
      },
    },
    async (
      request: FastifyRequest<{ Body: CreateBtcDeposit }>,
      reply: FastifyReply,
    ) => {
      assertClientAccess(request, request.body.clientId);

      if (request.body.basketId) {
        const basket = getAccessibleBasket(request, store, request.body.basketId);
        if (basket.clientId !== request.body.clientId) {
          throw new AppError(400, 'CLIENT_MISMATCH', 'Deposit client does not match basket client');
        }
        if (basket.chain138VaultAddress !== request.body.chain138VaultAddress) {
          throw new AppError(
            400,
            'VAULT_ADDRESS_MISMATCH',
            'Deposit vault address does not match the selected basket',
          );
        }
      }

      const result = store.createDeposit(request.body);
      return reply.status(201).send(result);
    },
  );

  server.get(
    '/api/v1/btc-deposits/:id',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'Get BTC deposit status',
        tags: ['jewelry-box'],
      },
    },
    async (
      request: FastifyRequest<{ Params: { id: string } }>,
    ) => {
      const deposit = store.getDeposit(request.params.id);
      if (!deposit) {
        throw new AppError(404, 'NOT_FOUND', 'Deposit not found');
      }

      assertClientAccess(request, deposit.clientId);
      return { deposit };
    },
  );

  server.get(
    '/api/v1/holdings',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'List basket holdings derived from underlying allocations',
        tags: ['jewelry-box'],
      },
    },
    async (
      request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>,
    ) => {
      if (request.query.basketId) {
        const basket = getAccessibleBasket(request, store, request.query.basketId);
        if (request.query.clientId && basket.clientId !== request.query.clientId) {
          throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket');
        }
      }

      const holdings = store.listHoldings({
        ...request.query,
        clientId: resolveClientScope(request, request.query.clientId),
      });
      return { holdings };
    },
  );

  server.get(
    '/api/v1/rebalances',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'List planned or queued rebalances for jewelry-box baskets',
        tags: ['jewelry-box'],
      },
    },
    async (
      request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>,
    ) => {
      if (request.query.basketId) {
        const basket = getAccessibleBasket(request, store, request.query.basketId);
        if (request.query.clientId && basket.clientId !== request.query.clientId) {
          throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket');
        }
      }

      const rebalances = store.listRebalances({
        ...request.query,
        clientId: resolveClientScope(request, request.query.clientId),
      });
      return { rebalances };
    },
  );

  server.post(
    '/api/v1/withdrawals/bridge',
    {
      preHandler: [authenticateJWT as never],
      schema: {
        description: 'Request a public-chain bridge withdrawal from a jewelry-box basket',
        tags: ['jewelry-box'],
        body: {
          type: 'object',
          additionalProperties: false,
          required: ['clientId', 'basketId', 'destinationChainId', 'destinationAddress', 'amount'],
          properties: {
            clientId: { type: 'string', minLength: 1 },
            basketId: { type: 'string', minLength: 1 },
            sourceSymbol: { type: 'string', minLength: 2, maxLength: 16 },
            destinationSymbol: { type: 'string', minLength: 2, maxLength: 16 },
            destinationChainId: { type: 'integer', minimum: 1 },
            destinationAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
            amount: { type: 'string', minLength: 1 },
          },
        },
      },
    },
    async (
      request: FastifyRequest<{ Body: CreateBridgeWithdrawal }>,
      reply: FastifyReply,
    ) => {
      const basket = getAccessibleBasket(request, store, request.body.basketId);
      if (basket.clientId !== request.body.clientId) {
        throw new AppError(400, 'CLIENT_MISMATCH', 'Withdrawal client does not match basket client');
      }

      const withdrawal = store.createBridgeWithdrawal(
        createDefaultBridgeWithdrawal(request.body as CreateBridgeWithdrawal),
      );
      return reply.status(201).send({ withdrawal });
    },
  );
}

export async function createServer(options: CreateServerOptions = {}): Promise<FastifyInstance> {
  if (env.DATABASE_URL) {
    getPool({ connectionString: env.DATABASE_URL });
  }

  const server: any = Fastify({
    logger: logger as any,
    requestIdLogLabel: 'requestId',
    disableRequestLogging: false,
  });

  await registerDocs(server, options.disableSwagger);
  await registerSecurityPlugins(server as any);
  addCorrelationId(server);
  addRequestLogging(server);
  server.setErrorHandler(errorHandler as any);

  registerHealthRoute(server);
  registerLedgerRoute(server);
  registerPaymentsRoute(server, createPaymentGateway());
  registerJewelryBoxRoutes(server, options.jewelryBoxStore || new JewelryBoxStore());

  return server as FastifyInstance;
}

export async function start(): Promise<void> {
  try {
    const server = await createServer();
    const port = env.PORT || 4003;
    await server.listen({ port, host: '0.0.0.0' });
    logger.info({ port }, 'Finance service listening');
  } catch (error) {
    logger.error({ err: error }, 'Failed to start server');
    process.exit(1);
  }
}

const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url);

if (isDirectExecution) {
  void start();
}
