import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { Cron } from '@nestjs/schedule';
import { firstValueFrom } from 'rxjs';
import { DataSource } from 'typeorm';
import { RedisEventBus } from '@it0/events';
import { EventPatterns, TenantContextService } from '@it0/common';
import { StandingOrderRepository } from '../../infrastructure/repositories/standing-order.repository';
import { StandingOrderExecutionRepository } from '../../infrastructure/repositories/standing-order-execution.repository';
import { StandingOrder } from '../entities/standing-order.entity';
import { StandingOrderExecution } from '../entities/standing-order-execution.entity';

@Injectable()
export class StandingOrderExecutorService implements OnModuleInit {
  private readonly logger = new Logger(StandingOrderExecutorService.name);
  private readonly agentServiceUrl: string;

  constructor(
    private readonly standingOrderRepo: StandingOrderRepository,
    private readonly executionRepo: StandingOrderExecutionRepository,
    private readonly eventBus: RedisEventBus,
    private readonly configService: ConfigService,
    private readonly httpService: HttpService,
    private readonly dataSource: DataSource,
  ) {
    this.agentServiceUrl = this.configService.get<string>(
      'AGENT_SERVICE_URL',
      'http://localhost:3001',
    );
  }

  async onModuleInit(): Promise<void> {
    this.logger.log('Subscribing to event-driven standing order triggers...');

    try {
      await this.eventBus.subscribe(
        EventPatterns.ALERT_FIRED,
        'standing-order-executor',
        `executor-${crypto.randomUUID().slice(0, 8)}`,
        async (event) => {
          try {
            this.logger.debug(`Received ALERT_FIRED event: ${event.id}`);
            await this.handleEventTrigger(EventPatterns.ALERT_FIRED, event);
          } catch (err) {
            this.logger.error(`ALERT_FIRED handler error: ${err}`);
          }
        },
      );

      await this.eventBus.subscribe(
        EventPatterns.STANDING_ORDER_TRIGGERED,
        'standing-order-executor',
        `executor-${crypto.randomUUID().slice(0, 8)}`,
        async (event) => {
          try {
            this.logger.debug(`Received STANDING_ORDER_TRIGGERED event: ${event.id}`);
            await this.handleEventTrigger(EventPatterns.STANDING_ORDER_TRIGGERED, event);
          } catch (err) {
            this.logger.error(`STANDING_ORDER_TRIGGERED handler error: ${err}`);
          }
        },
      );
    } catch (err) {
      this.logger.error(`Failed to subscribe to events: ${err}`);
    }
  }

  /**
   * Cron scanning: runs every minute to evaluate cron-based standing orders.
   * Must iterate all tenants since cron jobs run outside HTTP request context.
   */
  @Cron('*/1 * * * *')
  async scanCronOrders(): Promise<void> {
    let tenants: { id: string; name: string; slug: string }[];
    try {
      const result = await this.dataSource.query(
        `SELECT id, name, slug FROM public.tenants WHERE status = 'active'`,
      );
      tenants = result;
    } catch (err) {
      this.logger.error(`Failed to query tenants: ${err}`);
      return;
    }

    if (tenants.length === 0) return;

    const now = new Date();
    for (const tenant of tenants) {
      try {
        await TenantContextService.run(
          {
            tenantId: tenant.id,
            tenantName: tenant.name,
            plan: 'pro',
            schemaName: `it0_t_${tenant.slug}`,
            maxServers: -1,
            maxUsers: -1,
            maxStandingOrders: -1,
            maxAgentTokensPerMonth: -1,
          },
          async () => {
            const activeOrders =
              await this.standingOrderRepo.findByStatus('active');
            const cronOrders = activeOrders.filter(
              (order) =>
                order.trigger.type === 'cron' && order.trigger.cronExpression,
            );

            for (const order of cronOrders) {
              if (
                this.cronMatchesCurrentMinute(
                  order.trigger.cronExpression!,
                  now,
                )
              ) {
                this.logger.log(
                  `Cron match for standing order "${order.name}" (${order.id}) tenant=${tenant.id}`,
                );
                await this.executeOrder(order, {
                  triggerType: 'cron',
                  cronExpression: order.trigger.cronExpression,
                  firedAt: now.toISOString(),
                });
              }
            }
          },
        );
      } catch (err) {
        this.logger.error(
          `Scheduler error for tenant ${tenant.id}: ${err}`,
        );
      }
    }
  }

  /**
   * Handles an incoming event and matches it against event-triggered standing orders.
   * Events from Redis carry tenantId — look up slug for correct schema name.
   */
  private async handleEventTrigger(
    eventType: string,
    event: Record<string, any>,
  ): Promise<void> {
    const tenantId = event.tenantId;
    if (!tenantId) {
      this.logger.warn(`Event ${event.id} has no tenantId, skipping`);
      return;
    }

    let tenant: { id: string; name: string; slug: string } | undefined;
    try {
      const rows = await this.dataSource.query(
        `SELECT id, name, slug FROM public.tenants WHERE id = $1 AND status = 'active'`,
        [tenantId],
      );
      tenant = rows[0];
    } catch (err) {
      this.logger.error(`Failed to look up tenant ${tenantId}: ${err}`);
      return;
    }

    if (!tenant) {
      this.logger.warn(`Tenant ${tenantId} not found or inactive, skipping event`);
      return;
    }

    try {
      await TenantContextService.run(
        {
          tenantId: tenant.id,
          tenantName: tenant.name,
          plan: 'pro',
          schemaName: `it0_t_${tenant.slug}`,
          maxServers: -1,
          maxUsers: -1,
          maxStandingOrders: -1,
          maxAgentTokensPerMonth: -1,
        },
        async () => {
          const activeOrders =
            await this.standingOrderRepo.findByStatus('active');
          const matchingOrders = activeOrders.filter(
            (order) =>
              order.trigger.type === 'event' &&
              order.trigger.eventType === eventType,
          );

          for (const order of matchingOrders) {
            this.logger.log(
              `Event match for standing order "${order.name}" (${order.id}) on event ${eventType}`,
            );
            await this.executeOrder(order, {
              triggerType: 'event',
              eventType,
              eventId: event.id,
              eventPayload: event.payload,
            });
          }
        },
      );
    } catch (err) {
      this.logger.error(
        `Event trigger error for tenant ${tenant.id}: ${err}`,
      );
    }
  }

  /**
   * Executes a standing order: creates an execution record, calls agent-service,
   * updates status, and escalates if needed.
   */
  async executeOrder(
    order: StandingOrder,
    triggerContext: Record<string, unknown>,
  ): Promise<StandingOrderExecution> {
    const executionId = crypto.randomUUID();
    const startedAt = new Date();

    const execution: StandingOrderExecution = {
      id: executionId,
      tenantId: order.tenantId,
      orderId: order.id,
      triggerType: triggerContext.triggerType as string,
      triggerContext,
      status: 'running',
      actionsTaken: [],
      startedAt,
    };

    await this.executionRepo.save(execution);

    try {
      const response = await firstValueFrom(
        this.httpService.post(`${this.agentServiceUrl}/api/v1/agent/tasks`, {
          tenantId: order.tenantId,
          orderId: order.id,
          executionId,
          instructions: order.agentPrompt,
          skills: order.agentSkills ?? [],
          runbookId: order.runbookId,
          maxRiskLevel: order.maxRiskLevel,
          maxTurns: order.maxTurns,
          maxBudgetUsd: order.maxBudgetUsd,
          targets: order.targets,
          allowedActions: order.decisionBoundary.allowedActions,
          triggerContext,
        }),
      );

      const result = response.data;
      execution.sessionId = result.sessionId;
      execution.resultSummary = result.summary;
      execution.actionsTaken = result.actionsTaken ?? [];
      execution.status = result.status === 'success' ? 'completed' : 'failed';
      execution.completedAt = new Date();
      execution.durationMs = execution.completedAt.getTime() - startedAt.getTime();

      await this.executionRepo.save(execution);

      // Check escalation conditions
      if (this.shouldEscalate(order, execution, result)) {
        await this.triggerEscalation(order, execution);
      }

      // Publish execution completed event
      await this.eventBus.publish(EventPatterns.STANDING_ORDER_EXECUTED, {
        id: crypto.randomUUID(),
        tenantId: order.tenantId,
        timestamp: new Date(),
        source: 'ops-service',
        type: 'standing.order.executed',
        payload: {
          orderId: order.id,
          executionId,
          status: execution.status,
          summary: execution.resultSummary,
        },
      });
    } catch (error: any) {
      execution.status = 'failed';
      execution.error = error.message ?? 'Unknown error during execution';
      execution.completedAt = new Date();
      execution.durationMs = execution.completedAt.getTime() - startedAt.getTime();

      await this.executionRepo.save(execution);

      this.logger.error(
        `Standing order "${order.name}" (${order.id}) execution failed: ${execution.error}`,
      );

      // Escalate on failure if escalation rules exist
      if (order.decisionBoundary.escalationRules?.length > 0) {
        await this.triggerEscalation(order, execution);
      }
    }

    return execution;
  }

  /**
   * Determines if the execution result meets any escalation conditions.
   */
  private shouldEscalate(
    order: StandingOrder,
    execution: StandingOrderExecution,
    result: Record<string, any>,
  ): boolean {
    if (!order.decisionBoundary.escalateConditions?.length) {
      return false;
    }

    for (const condition of order.decisionBoundary.escalateConditions) {
      if (condition === 'on_failure' && execution.status === 'failed') {
        return true;
      }
      if (condition === 'on_awaiting_human' && result.status === 'awaiting_human') {
        return true;
      }
      if (condition === 'on_risk_exceeded' && result.riskLevelExceeded) {
        return true;
      }
    }

    return false;
  }

  /**
   * Publishes an ESCALATION_TRIGGERED event for the comm-service to handle.
   */
  private async triggerEscalation(
    order: StandingOrder,
    execution: StandingOrderExecution,
  ): Promise<void> {
    execution.escalatedAt = new Date();
    execution.escalationChannel =
      order.decisionBoundary.escalationRules?.[0]?.channel ?? 'im';
    execution.status = 'awaiting_human';

    await this.executionRepo.save(execution);

    await this.eventBus.publish(EventPatterns.ESCALATION_TRIGGERED, {
      id: crypto.randomUUID(),
      tenantId: order.tenantId,
      timestamp: new Date(),
      source: 'ops-service',
      type: 'escalation.triggered',
      payload: {
        orderId: order.id,
        executionId: execution.id,
        orderName: order.name,
        channel: execution.escalationChannel,
        priority:
          order.decisionBoundary.escalationRules?.[0]?.priority ?? 'high',
        escalationPolicyId: order.escalationPolicyId,
        reason: execution.error ?? execution.resultSummary ?? 'Escalation condition met',
      },
    });

    this.logger.warn(
      `Escalation triggered for standing order "${order.name}" (${order.id}), ` +
        `execution ${execution.id}, channel: ${execution.escalationChannel}`,
    );
  }

  /**
   * Evaluates whether a cron expression matches the current minute.
   * Supports standard 5-field cron: minute hour day-of-month month day-of-week.
   */
  private cronMatchesCurrentMinute(expression: string, now: Date): boolean {
    try {
      const parts = expression.trim().split(/\s+/);
      if (parts.length !== 5) return false;

      const [minExpr, hourExpr, domExpr, monExpr, dowExpr] = parts;
      const minute = now.getMinutes();
      const hour = now.getHours();
      const dayOfMonth = now.getDate();
      const month = now.getMonth() + 1;
      const dayOfWeek = now.getDay(); // 0 = Sunday

      return (
        this.fieldMatches(minExpr, minute, 0, 59) &&
        this.fieldMatches(hourExpr, hour, 0, 23) &&
        this.fieldMatches(domExpr, dayOfMonth, 1, 31) &&
        this.fieldMatches(monExpr, month, 1, 12) &&
        this.fieldMatches(dowExpr, dayOfWeek, 0, 6)
      );
    } catch {
      this.logger.error(`Invalid cron expression: ${expression}`);
      return false;
    }
  }

  /**
   * Matches a single cron field against a value.
   * Supports: *, specific numbers, step values (asterisk/N), and comma-separated lists.
   */
  private fieldMatches(
    field: string,
    value: number,
    min: number,
    max: number,
  ): boolean {
    if (field === '*') return true;

    // Handle comma-separated values
    if (field.includes(',')) {
      return field.split(',').some((part) => this.fieldMatches(part.trim(), value, min, max));
    }

    // Handle step values: */N
    if (field.startsWith('*/')) {
      const step = parseInt(field.slice(2), 10);
      if (isNaN(step) || step <= 0) return false;
      return (value - min) % step === 0;
    }

    // Handle exact value
    const exact = parseInt(field, 10);
    if (isNaN(exact)) return false;
    return exact === value;
  }
}
