// packages/functions/src/firebase/baseFunction.ts

/**
 * @fileoverview Base Firebase function that handles all common concerns
 * @description Rate limiting, monitoring, authentication, validation - all in one place
 *
 * @version 0.0.1
 * @since 0.0.1
 * @author AMBROISE PARK Consulting
 */

import { logger } from 'firebase-functions/v2';
import { onCall, type CallableRequest } from 'firebase-functions/v2/https';
import * as v from 'valibot';

import { handleError } from '../shared/errorHandling.js';
import { recordPaymentMetrics } from '../shared/utils/internal/monitoring.js';
import {
  checkRateLimitWithFirestore,
  DEFAULT_RATE_LIMITS,
} from '../shared/utils/internal/rateLimiter.js';
import { assertAuthenticated } from '../shared/utils.js';

/**
 * Base Firebase function that handles all common concerns
 * Users just provide their business logic
 *
 * @version 0.0.1
 * @since 0.0.1
 * @author AMBROISE PARK Consulting
 */
export function createBaseFunction<TRequest, TResponse>(
  config: any,
  schema: v.BaseSchema<unknown, TRequest, v.BaseIssue<unknown>>,
  operation: string,
  businessLogic: (
    data: TRequest,
    context: {
      uid: string;
      request: CallableRequest<TRequest>;
    }
  ) => Promise<TResponse>
) {
  // Validate schema at function creation time (framework-level robustness)
  if (!schema) {
    const error = new Error(
      `Schema is undefined for ${operation}. Ensure the schema is properly imported and exported.`
    );
    logger.error(
      `[createBaseFunction] Schema validation failed at creation time`,
      {
        operation,
        schemaType: typeof schema,
        schemaValue: schema,
        schemaConstructor: (schema as any)?.constructor?.name,
      }
    );
    throw error;
  }

  // Validate schema is a valid Valibot schema
  if (typeof schema !== 'object' || schema === null) {
    const error = new Error(
      `Invalid schema type for ${operation}. Expected Valibot schema object, got ${typeof schema}.`
    );
    logger.error(`[createBaseFunction] Invalid schema type`, {
      operation,
      schemaType: typeof schema,
      schemaValue: schema,
    });
    throw error;
  }

  return onCall<TRequest>(
    config,
    async (request: CallableRequest<TRequest>) => {
      try {
        // Log incoming request for debugging
        logger.info(`[${operation}] Request received`, {
          hasAuth: !!request.auth,
          userId: request.auth?.uid || 'anonymous',
          hasData: request.data !== undefined && request.data !== null,
          dataKeys: request.data ? Object.keys(request.data) : [],
          dataPreview: request.data
            ? JSON.stringify(request.data).substring(0, 200)
            : 'null',
        });

        // App Check monitoring (logs missing tokens for visibility)
        // Actual enforcement is via enforceAppCheck in config
        if (!request.app) {
          logger.warn(`[AppCheck] Missing token for ${operation}`, {
            hasAuth: !!request.auth,
            userId: request.auth?.uid || 'anonymous',
          });
        }

        // Schema should already be validated at function creation time
        // This is a defensive check in case schema becomes undefined at runtime (shouldn't happen)
        if (!schema) {
          logger.error(
            `[${operation}] Schema became undefined at runtime (this should not happen)`,
            {
              operation,
              schemaType: typeof schema,
              requestData: request.data,
            }
          );
          throw new Error(
            `Schema is undefined for ${operation} - this indicates a bundling/import issue`
          );
        }
        if (request.data === undefined || request.data === null) {
          logger.error(`Request data is missing for ${operation}`, {
            operation,
            dataValue: request.data,
            hasAuth: !!request.auth,
            userId: request.auth?.uid,
          });
          throw new Error(
            `Request data is required for ${operation} but was ${request.data === null ? 'null' : 'undefined'}`
          );
        }

        let validatedData: TRequest;
        try {
          validatedData = v.parse(schema, request.data);
        } catch (parseError) {
          logger.error(`Schema validation failed for ${operation}`, {
            error:
              parseError instanceof Error
                ? parseError.message
                : String(parseError),
            data: request.data,
          });
          throw parseError;
        }

        // Verify authentication
        const uid = assertAuthenticated(request.auth);

        // Rate limiting
        const rateLimitKey = `${operation}_${uid}`;
        const rateLimitConfig =
          (DEFAULT_RATE_LIMITS as any)[operation] ||
          DEFAULT_RATE_LIMITS.checkout;
        const rateLimitResult = await checkRateLimitWithFirestore(
          rateLimitKey,
          rateLimitConfig
        );

        if (!rateLimitResult.allowed) {
          logger.warn(`Rate limit exceeded for ${operation}`, {
            userId: uid,
            remaining: rateLimitResult.remaining,
            resetAt: rateLimitResult.resetAt,
          });
          throw new Error(
            `Rate limit exceeded. Try again in ${rateLimitResult.blockRemainingSeconds} seconds.`
          );
        }

        // Call user's business logic
        const result = await businessLogic(validatedData, { uid, request });

        // Record metrics
        await recordPaymentMetrics({
          operation,
          userId: uid,
          status: 'success',
          timestamp: new Date().toISOString(),
          metadata: {
            requestId: request.rawRequest.headers['x-request-id'] || 'unknown',
          },
        });

        return result;
      } catch (error) {
        // Record error metrics
        await recordPaymentMetrics({
          operation,
          userId: request.auth?.uid || 'anonymous',
          status: 'failed' as const,
          timestamp: new Date().toISOString(),
          metadata: {
            error: error instanceof Error ? error.message : 'Unknown error',
            requestId: request.rawRequest.headers['x-request-id'] || 'unknown',
          },
        });

        throw handleError(error);
      }
    }
  );
}
