import type { ExecutionContext } from '../../base/types'
import type { BaseIntegrationAdapter } from './base-integration-adapter'
import type { RateLimiter, RetryPolicy } from '../../../../platform/resilience'
import { ToolingError } from '../types'
import { withRetry, InMemoryRateLimiter, DEFAULT_RETRY_POLICY } from '../../../../platform/resilience'
import {
  INTEGRATION_RATE_LIMIT_CAPACITY,
  INTEGRATION_RATE_LIMIT_WINDOW_MS
} from '../../../../platform/constants/resilience'
import { getToolServices } from '../registry'

/**
 * Integration Service
 *
 * Central service for managing integration adapters, rate limiting, and retry.
 * Credentials accessed via global tool services registry.
 *
 * Pattern: Active Record (justified - owns state + behavior)
 *
 * @example
 * const service = new IntegrationService(
 *   new Map([['gmail', gmailAdapter]])
 * )
 *
 * const result = await service.call('gmail', 'sendEmail', params, 'gmail-cred', context)
 */
export class IntegrationService {
  private readonly rateLimiter: RateLimiter
  private readonly retryPolicy: RetryPolicy

  constructor(public readonly adapters: Map<string, BaseIntegrationAdapter>) {
    this.rateLimiter = new InMemoryRateLimiter(INTEGRATION_RATE_LIMIT_CAPACITY, INTEGRATION_RATE_LIMIT_WINDOW_MS)
    this.retryPolicy = DEFAULT_RETRY_POLICY
  }

  /**
   * Call integration method with full orchestration
   *
   * Orchestrates the full integration call flow:
   * 1. Validate execution context
   * 2. Validate adapter exists
   * 3. Check rate limit (per organization per integration)
   * 4. Get credentials from registry
   * 5. Validate credentials
   * 6. Call adapter with retry
   *
   * @param integration - Integration adapter type (e.g., 'gmail', 'slack')
   * @param method - Method name (e.g., 'sendEmail', 'postMessage')
   * @param params - Method parameters
   * @param credentialName - Credential name from credentials table
   * @param context - Execution context (required for multi-tenant isolation)
   * @returns Method result
   * @throws ToolingError if call fails
   */
  async call(
    integration: string,
    method: string,
    params: unknown,
    credentialName: string,
    context: ExecutionContext | undefined
  ): Promise<unknown> {
    // 1. Validate execution context (required for multi-tenant isolation)
    if (!context) {
      throw new ToolingError('validation_error', 'ExecutionContext required for integration calls', {
        integration,
        method,
        credentialName
      })
    }

    // 2. Validate adapter exists
    const adapter = this.adapters.get(integration)
    if (!adapter) {
      throw new ToolingError('adapter_not_found', `Integration adapter not found: ${integration}`, {
        integration,
        availableAdapters: Array.from(this.adapters.keys())
      })
    }

    // 3. Check rate limit (per organization per integration)
    const rateLimitKey = `${context.organizationId}:${integration}`
    const allowed = await this.rateLimiter.allow(rateLimitKey)
    if (!allowed) {
      const remaining = await this.rateLimiter.remaining(rateLimitKey)
      throw new ToolingError('rate_limit_exceeded', 'Rate limit exceeded', {
        integration,
        organizationId: context.organizationId,
        remaining
      })
    }

    // 4. Get credentials from registry (per-execution cache keyed by credential name).
    // The store is an opportunistic perf cache — never a load-bearing contract.
    // Optional chaining lets a missing/undefined store fall through to the fetch path
    // instead of crashing with "Cannot read properties of undefined (reading 'get')".
    const cacheKey = `__cred_cache__:${credentialName}`
    let credentials = context.store?.get(cacheKey) as Record<string, unknown> | undefined

    if (!credentials) {
      const { credentialsService } = getToolServices()
      if (!credentialsService) {
        throw new ToolingError(
          'service_unavailable',
          'CredentialsService not initialized. Ensure toolServicesRegistry.initialize() was called in apps/api/src/main.ts',
          { integration, credentialName }
        )
      }

      const fetched = await credentialsService.getCredential(context.organizationId, credentialName)
      if (!fetched) {
        throw new ToolingError('credentials_missing', `No credentials found for credential name: ${credentialName}`, {
          integration,
          credentialName,
          organizationId: context.organizationId
        })
      }

      context.store?.set(cacheKey, fetched)
      credentials = fetched
    }

    // Log credential retrieval for OAuth integrations
    const isOAuth = !!credentials.provider
    if (isOAuth) {
      const creds = credentials
      context.logger?.debug(
        `OAuth credential retrieved: integration=${integration}, credentialName=${credentialName}, provider=${creds.provider}, hasRefreshToken=${!!creds.refreshToken}, hasExpiresAt=${!!creds.expiresAt}, expiresAt=${creds.expiresAt}`
      )
    }

    // 5. Validate credentials
    const isValid = adapter.validateCredentials(credentials)
    if (!isValid) {
      throw new ToolingError('credentials_invalid', `Invalid credentials for integration: ${integration}`, {
        integration,
        organizationId: context.organizationId
      })
    }

    // 6. Call adapter with retry (pass credentialName in context for token refresh persistence)
    const contextWithCredential: ExecutionContext = {
      ...context,
      credentialName
    }
    return withRetry(() => adapter.call(method, params, credentials, contextWithCredential), this.retryPolicy)
  }

  /**
   * Get adapter by name
   * @param integration - Integration name
   * @returns Adapter or undefined
   */
  getAdapter(integration: string): BaseIntegrationAdapter | undefined {
    return this.adapters.get(integration)
  }

  /**
   * Register adapter
   * @param adapter - Adapter to register
   */
  registerAdapter(adapter: BaseIntegrationAdapter): void {
    this.adapters.set(adapter.name, adapter)
  }

  /**
   * Unregister adapter
   * @param integration - Integration name to unregister
   */
  unregisterAdapter(integration: string): void {
    this.adapters.delete(integration)
  }
}
