// Policy Engine Service
// Makes runtime policy decisions based on entitlements, rules, and conditions

import { v4 as uuidv4 } from 'uuid';
import type { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { DbisError, ErrorCode } from '@/shared/types';
import { logger } from '@/infrastructure/monitoring/logger';
import {
  PolicyRule,
  PolicyDecisionRequest,
  PolicyDecisionResponse,
  CreatePolicyRuleRequest,
  CapabilityState,
} from '@/shared/solacenet/types';
import { expressionEvaluator } from './expression-evaluator';
import { entitlementsService } from '../entitlements/entitlements.service';
import { capabilityRegistryService } from '../registry/capability-registry.service';

type PolicyRedisClient = {
  isOpen?: boolean;
  connect(): Promise<void>;
  get(key: string): Promise<string | null>;
  setEx(key: string, ttlSeconds: number, value: string): Promise<unknown>;
  keys(pattern: string): Promise<string[]>;
  del(keys: string | string[]): Promise<unknown>;
  on(event: 'error', listener: (error: Error) => void): void;
};

let redisClient: PolicyRedisClient | null = null;
let redisModuleChecked = false;
let redisModuleAvailable = false;
let redisConnectPromise: Promise<PolicyRedisClient | null> | null = null;
let redisMissingLogged = false;
let redisConnectionFailureLogged = false;

function loadRedisModule():
  | {
      createClient(options: { url: string }): PolicyRedisClient;
    }
  | null {
  if (redisModuleChecked) {
    return redisModuleAvailable ? require('redis') : null;
  }

  redisModuleChecked = true;

  try {
    const redis = require('redis') as { createClient(options: { url: string }): PolicyRedisClient };
    redisModuleAvailable = true;
    return redis;
  } catch (error) {
    if (!redisMissingLogged) {
      logger.warn('Redis client module is unavailable, policy decisions will not be cached', {
        error: error instanceof Error ? error.message : String(error),
      });
      redisMissingLogged = true;
    }
    redisModuleAvailable = false;
    return null;
  }
}

export class PolicyEngineService {
  private readonly CACHE_TTL = 120; // 2 minutes default TTL

  private async getRedisClient(): Promise<PolicyRedisClient | null> {
    const redisUrl = process.env.REDIS_URL?.trim();
    if (!redisUrl) {
      return null;
    }

    if (redisClient?.isOpen) {
      return redisClient;
    }

    if (redisConnectPromise) {
      return redisConnectPromise;
    }

    const redis = loadRedisModule();
    if (!redis) {
      return null;
    }

    redisConnectPromise = (async () => {
      try {
        const client = redisClient ?? redis.createClient({ url: redisUrl });
        if (!redisClient) {
          client.on('error', (error) => {
            logger.warn('Redis client error, policy cache unavailable', {
              error: error.message,
            });
          });
        }

        if (!client.isOpen) {
          await client.connect();
        }

        redisClient = client;
        redisConnectionFailureLogged = false;
        return redisClient;
      } catch (error) {
        redisClient = null;
        if (!redisConnectionFailureLogged) {
          logger.warn('Redis connection failed, policy decisions will not be cached', {
            error: error instanceof Error ? error.message : String(error),
          });
          redisConnectionFailureLogged = true;
        }
        return null;
      } finally {
        redisConnectPromise = null;
      }
    })();

    return redisConnectPromise;
  }

  /**
   * Make a policy decision
   */
  async decide(request: PolicyDecisionRequest): Promise<PolicyDecisionResponse> {
    const decisionId = uuidv4();

    // Check cache first
    const cacheKey = this.getCacheKey(request);
    const cached = await this.getCachedDecision(cacheKey);
    if (cached) {
      logger.debug(`Policy decision cache hit: ${cacheKey}`);
      return { ...cached, decisionId };
    }

    // Get capability
    const capability = await capabilityRegistryService.getCapabilityById(request.capabilityId);
    if (!capability) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Capability ${request.capabilityId} not found`);
    }

    // Check entitlement
    const entitlement = await entitlementsService.checkEntitlement({
      tenantId: request.tenantId,
      programId: request.programId,
      capabilityId: request.capabilityId,
      region: request.region,
      channel: request.channel,
      actor: request.actor,
    });

    // Determine base state
    let baseState: CapabilityState = capability.defaultState;
    if (entitlement) {
      baseState = entitlement.stateOverride || capability.defaultState;
    }

    // If disabled or suspended, deny immediately
    if (baseState === CapabilityState.DISABLED || baseState === CapabilityState.SUSPENDED) {
      const decision: PolicyDecisionResponse = {
        allowed: false,
        mode: baseState,
        reasonCode: `capability_${baseState}`,
        decisionId,
      };
      await this.cacheDecision(cacheKey, decision);
      return decision;
    }

    // If pilot mode, check allowlist
    if (baseState === CapabilityState.PILOT) {
      if (!entitlement || !entitlement.allowlist.includes(request.actor || '')) {
        const decision: PolicyDecisionResponse = {
          allowed: false,
          mode: baseState,
          reasonCode: 'pilot_mode_not_allowed',
          decisionId,
        };
        await this.cacheDecision(cacheKey, decision);
        return decision;
      }
    }

    // Evaluate policy rules (in priority order)
    const rules = await this.getApplicableRules(request);
    const evaluatedRules: string[] = [];
    let finalDecision: PolicyDecisionResponse | null = null;

    for (const rule of rules) {
      evaluatedRules.push(rule.ruleId);

      // Evaluate condition
      const conditionMet = expressionEvaluator.evaluate(rule.condition, {
        ...request.context,
        tenantId: request.tenantId,
        programId: request.programId,
        region: request.region,
        channel: request.channel,
        actor: request.actor,
      });

      if (conditionMet) {
        if (rule.decision === 'deny') {
          finalDecision = {
            allowed: false,
            mode: baseState,
            reasonCode: `policy_rule_denied:${rule.ruleId}`,
            decisionId,
            evaluatedRules,
          };
          break;
        } else if (rule.decision === 'limit') {
          finalDecision = {
            allowed: true,
            mode: baseState,
            limits: rule.limits,
            reasonCode: `policy_rule_limited:${rule.ruleId}`,
            decisionId,
            evaluatedRules,
          };
          // Continue to check for deny rules
        } else if (rule.decision === 'allow') {
          finalDecision = {
            allowed: true,
            mode: baseState,
            reasonCode: `policy_rule_allowed:${rule.ruleId}`,
            decisionId,
            evaluatedRules,
          };
          // Continue to check for deny rules (deny takes precedence)
        }
      }
    }

    // Default decision if no rules matched
    if (!finalDecision) {
      finalDecision = {
        allowed: baseState === CapabilityState.ENABLED || baseState === CapabilityState.DRAIN,
        mode: baseState,
        reasonCode: 'default_decision',
        decisionId,
        evaluatedRules,
      };
    }

    // Cache the decision
    await this.cacheDecision(cacheKey, finalDecision);

    // Log the decision
    logger.info(`Policy decision made`, {
      decisionId,
      capabilityId: request.capabilityId,
      tenantId: request.tenantId,
      allowed: finalDecision.allowed,
      mode: finalDecision.mode,
    });

    return finalDecision;
  }

  /**
   * Get applicable policy rules for a request
   */
  private async getApplicableRules(request: PolicyDecisionRequest): Promise<PolicyRule[]> {
    const rules = await prisma.solacenet_policy_rule.findMany({
      where: {
        capabilityId: request.capabilityId,
        status: 'active',
        OR: [
          { scope: 'global' },
          { scope: 'tenant', scopeValue: request.tenantId },
          { scope: 'program', scopeValue: request.programId || null },
          { scope: 'region', scopeValue: request.region || null },
          { scope: 'channel', scopeValue: request.channel || null },
        ],
      },
      orderBy: {
        priority: 'asc',
      },
    });

    return rules.map((rule) => ({
      id: rule.id,
      ruleId: rule.ruleId,
      capabilityId: rule.capabilityId,
      scope: rule.scope as 'tenant' | 'program' | 'region' | 'channel' | 'global',
      scopeValue: rule.scopeValue || undefined,
      condition: rule.condition as Record<string, unknown>,
      decision: rule.decision as 'allow' | 'deny' | 'limit',
      limits: rule.limits as Record<string, unknown> | undefined,
      reason: rule.reason || undefined,
      ticket: rule.ticket || undefined,
      priority: rule.priority,
      status: rule.status as 'active' | 'inactive',
      metadata: rule.metadata as Record<string, unknown> | undefined,
      createdAt: rule.createdAt,
      updatedAt: rule.updatedAt,
    }));
  }

  /**
   * Create a policy rule
   */
  async createPolicyRule(request: CreatePolicyRuleRequest): Promise<PolicyRule> {
    // Check if rule ID already exists
    const existing = await prisma.solacenet_policy_rule.findUnique({
      where: { ruleId: request.ruleId },
    });

    if (existing) {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, `Policy rule ${request.ruleId} already exists`);
    }

    // Check if capability exists
    const capability = await capabilityRegistryService.getCapabilityById(request.capabilityId);
    if (!capability) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Capability ${request.capabilityId} not found`);
    }

    const rule = await prisma.solacenet_policy_rule.create({
      data: {
        id: uuidv4(),
        ruleId: request.ruleId,
        capabilityId: request.capabilityId,
        scope: request.scope,
        scopeValue: request.scopeValue || null,
        condition: request.condition as Prisma.InputJsonValue,
        decision: request.decision,
        limits:
          request.limits != null ? (request.limits as Prisma.InputJsonValue) : undefined,
        reason: request.reason || null,
        ticket: request.ticket || null,
        priority: request.priority || 100,
        status: 'active',
        metadata:
          request.metadata != null ? (request.metadata as Prisma.InputJsonValue) : undefined,
      },
    });

    logger.info(`Policy rule created: ${request.ruleId}`);
    return this.mapToPolicyRule(rule);
  }

  /**
   * Get all policy rules
   */
  async getPolicyRules(capabilityId?: string): Promise<PolicyRule[]> {
    const rules = await prisma.solacenet_policy_rule.findMany({
      where: {
        ...(capabilityId ? { capabilityId } : {}),
      },
      orderBy: [
        { priority: 'asc' },
        { createdAt: 'desc' },
      ],
    });

    return rules.map(this.mapToPolicyRule);
  }

  /**
   * Activate kill switch for a capability
   */
  async activateKillSwitch(capabilityId: string, reason?: string): Promise<void> {
    // Invalidate all cached decisions for this capability
    await this.invalidateCache(capabilityId);

    // Log the kill switch activation
    logger.warn(`Kill switch activated for capability: ${capabilityId}`, { reason });

    // Emit event (will be handled by event bus integration)
    // This will be implemented in the event bus integration
  }

  /**
   * Get cache key for a request
   */
  private getCacheKey(request: PolicyDecisionRequest): string {
    return `policy:decision:${request.tenantId}:${request.programId || 'none'}:${request.capabilityId}:${request.region || 'none'}:${request.channel || 'none'}:${request.actor || 'none'}`;
  }

  /**
   * Get cached decision
   */
  private async getCachedDecision(cacheKey: string): Promise<PolicyDecisionResponse | null> {
    const client = await this.getRedisClient();
    if (!client) {
      return null;
    }

    try {
      const cached = await client.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }
    } catch (error) {
      logger.warn('Failed to get cached decision', { error });
    }

    return null;
  }

  /**
   * Cache a decision
   */
  private async cacheDecision(cacheKey: string, decision: PolicyDecisionResponse): Promise<void> {
    const client = await this.getRedisClient();
    if (!client) {
      return;
    }

    try {
      await client.setEx(cacheKey, this.CACHE_TTL, JSON.stringify(decision));
    } catch (error) {
      logger.warn('Failed to cache decision', { error });
    }
  }

  /**
   * Invalidate cache for a capability
   */
  private async invalidateCache(capabilityId: string): Promise<void> {
    const client = await this.getRedisClient();
    if (!client) {
      return;
    }

    try {
      // Use pattern matching to find all keys for this capability
      const pattern = `policy:decision:*:*:${capabilityId}:*`;
      const keys = await client.keys(pattern);
      if (keys.length > 0) {
        await client.del(keys);
      }
    } catch (error) {
      logger.warn('Failed to invalidate cache', { error });
    }
  }

  /**
   * Map Prisma model to PolicyRule type
   */
  private mapToPolicyRule(rule: {
    id: string;
    ruleId: string;
    capabilityId: string;
    scope: string;
    scopeValue: string | null;
    condition: unknown;
    decision: string;
    limits: unknown;
    reason: string | null;
    ticket: string | null;
    priority: number;
    status: string;
    metadata: unknown;
    createdAt: Date;
    updatedAt: Date;
  }): PolicyRule {
    return {
      id: rule.id,
      ruleId: rule.ruleId,
      capabilityId: rule.capabilityId,
      scope: rule.scope as 'tenant' | 'program' | 'region' | 'channel' | 'global',
      scopeValue: rule.scopeValue || undefined,
      condition: rule.condition as Record<string, unknown>,
      decision: rule.decision as 'allow' | 'deny' | 'limit',
      limits: rule.limits as Record<string, unknown> | undefined,
      reason: rule.reason || undefined,
      ticket: rule.ticket || undefined,
      priority: rule.priority,
      status: rule.status as 'active' | 'inactive',
      metadata: rule.metadata as Record<string, unknown> | undefined,
      createdAt: rule.createdAt,
      updatedAt: rule.updatedAt,
    };
  }
}

export const policyEngineService = new PolicyEngineService();
