import db from '../database/models/index.js';
const { SLATracking, SLAConfiguration, SLABreach, Application, User, SLAReminder, SLAEscalationConfig } = db;
import { Op } from 'sequelize';
import { slaConfigLookupNames } from '../common/config/slaStageCatalog.js';
import { effectiveElapsedMs } from '../common/utils/slaBusinessTime.js';
import { NotificationService } from './NotificationService.js';
import { resolveRecipientsForRoles } from './slaGeographyResolver.js';
import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js';
import type { WorkflowActivityRequestType } from '../common/utils/workflowWorknote.js';
import {
  claimSlaNotificationDispatch,
  SlaDispatchKeys,
  updateSlaDispatchRecipientCount,
  wasSlaNotificationDispatched
} from './slaNotificationDispatch.service.js';
import { getFrontendBaseUrl } from '../common/utils/frontendUrl.js';

export type SlaTrackRef = {
    entityType: string;
    entityId: string;
    applicationId?: string | null;
    stageName: string;
};

export class SLAService {
  /** Stable idempotency key per reminder threshold (once per track). */
  private static reminderSentKey(reminder: { id?: string; timeValue: number; timeUnit: string }): string {
    if (reminder.id) return `reminder_sent_${reminder.id}`;
    return `reminder_sent_${reminder.timeValue}_${reminder.timeUnit}`;
  }

  private static escalationSentKey(level: number): string {
    return `esc_sent_L${level}`;
  }

  /** Sequelize JSONB must get a new object reference or updates may not persist. */
  private static async persistTrackMetadata(
    track: { metadata?: Record<string, unknown> | null; update: (payload: object) => Promise<unknown> },
    patch: Record<string, unknown>
  ): Promise<Record<string, unknown>> {
    const current =
      track.metadata && typeof track.metadata === 'object' && !Array.isArray(track.metadata)
        ? { ...track.metadata }
        : {};
    const next = { ...current, ...patch };
    await track.update({ metadata: next });
    track.metadata = next;
    return next;
  }

  private static async findConfigForStage(stageName: string) {
    const names = slaConfigLookupNames(stageName);
    return SLAConfiguration.findOne({
      where: { activityName: { [Op.in]: names }, isActive: true }
    });
  }

  private static resolveTrackRef(
    refOrAppId: string | SlaTrackRef,
    stageName?: string
  ): SlaTrackRef {
    if (typeof refOrAppId === 'string') {
      return {
        entityType: 'application',
        entityId: refOrAppId,
        applicationId: refOrAppId,
        stageName: stageName!
      };
    }
    return refOrAppId;
  }

  static async checkBreaches() {
    console.log('[SLA Service] Starting SLA status check...');
    const now = new Date();

    const activeTracking = await SLATracking.findAll({
      where: { isActive: true, endTime: null },
      include: [{ model: Application, as: 'application', required: false }]
    });

    for (const track of activeTracking) {
      const config = await this.findConfigForStage(track.stageName);
      const configWithChildren = config
        ? await SLAConfiguration.findByPk(config.id, {
        include: [
          { model: SLAReminder, as: 'reminders' },
          { model: SLAEscalationConfig, as: 'escalationConfigs' }
        ]
      })
        : null;

      if (!configWithChildren) continue;
      const configResolved = configWithChildren;

      const meta = track.metadata || {};
      if (meta.pausedAt) continue;

      const tatMs = this.getTatInMs(configResolved.tatHours, configResolved.tatUnit);
      const elapsedMs = effectiveElapsedMs(track, now.getTime());
      const deadline = new Date(new Date(track.startTime).getTime() + tatMs);

      if (!track.isBreached && elapsedMs < tatMs) {
        await this.processReminders(track, configResolved, now, deadline, tatMs, elapsedMs);
      } else if (!track.isBreached && elapsedMs >= tatMs) {
        await this.triggerBreach(track, now);
      } else if (track.isBreached) {
        await this.processEscalations(track, configResolved, now, deadline);
        await this.processRepeatOverdueReminder(track, now);
      }
    }
  }

  static async startTrack(refOrAppId: string | SlaTrackRef, stageName?: string) {
    const ref = this.resolveTrackRef(refOrAppId, stageName);
    console.log(
      `[SLA Service] Starting SLA track for ${ref.entityType}:${ref.entityId}, Stage: ${ref.stageName}`
    );

    const deactivateWhere: Record<string, unknown> = {
      entityId: ref.entityId,
      isActive: true,
      endTime: null
    };
    // F&F: multiple department timers run in parallel on the same settlement
    if (ref.entityType === 'fnf') {
      deactivateWhere.stageName = ref.stageName;
    }
    await SLATracking.update({ isActive: false, endTime: new Date() }, { where: deactivateWhere });

    const config = await this.findConfigForStage(ref.stageName);

    if (config) {
      await SLATracking.create({
        applicationId: ref.applicationId ?? null,
        entityType: ref.entityType,
        entityId: ref.entityId,
        stageName: ref.stageName,
        startTime: new Date(),
        isActive: true
      });
    }
  }

  /** Pause all active SLA timers for an entity (e.g. case On Hold). */
  static async pauseEntityTracks(entityType: string, entityId: string) {
    const tracks = await SLATracking.findAll({
      where: { entityType, entityId, isActive: true, endTime: null }
    });
    const pausedAt = new Date().toISOString();
    for (const track of tracks) {
      const meta = { ...(track.metadata || {}), pausedAt };
      await track.update({ metadata: meta });
    }
  }

  /** Resume paused SLA timers after hold / waiting period ends. */
  static async resumeEntityTracks(entityType: string, entityId: string) {
    const tracks = await SLATracking.findAll({
      where: { entityType, entityId, isActive: true, endTime: null }
    });
    const now = Date.now();
    for (const track of tracks) {
      const meta = { ...(track.metadata || {}) };
      if (!meta.pausedAt) continue;
      const pauseStart = new Date(String(meta.pausedAt)).getTime();
      meta.accumulatedPauseMs = Number(meta.accumulatedPauseMs || 0) + (now - pauseStart);
      delete meta.pausedAt;
      await track.update({ metadata: meta });
    }
  }

  static async stopTrack(refOrAppId: string | SlaTrackRef, stageName?: string) {
    const ref = this.resolveTrackRef(refOrAppId, stageName);
    console.log(
      `[SLA Service] Stopping SLA track for ${ref.entityType}:${ref.entityId}, Stage: ${ref.stageName}`
    );
    const stageNames = slaConfigLookupNames(ref.stageName);
    await SLATracking.update(
      { isActive: false, endTime: new Date() },
      {
        where: {
          entityId: ref.entityId,
          stageName: { [Op.in]: stageNames },
          isActive: true
        }
      }
    );
  }

  private static getTatInMs(value: number, unit: string): number {
    let factor = unit === 'days' ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
    if (process.env.DEBUG_SLA_FAST_MODE === 'true') {
      factor = factor / 60;
    }
    return value * factor;
  }

  private static async getCaseLabel(track: any): Promise<string> {
    if (track.application?.applicationId) return track.application.applicationId;
    const type = track.entityType;
    const id = track.entityId;
    if (type === 'termination') {
      const row = await db.TerminationRequest.findByPk(id, { attributes: ['requestId'] });
      return row?.requestId || id;
    }
    if (type === 'resignation') {
      const row = await db.Resignation.findByPk(id, { attributes: ['resignationId'] });
      return row?.resignationId || id;
    }
    if (type === 'relocation') {
      const row = await db.RelocationRequest.findByPk(id, { attributes: ['requestId'] });
      return row?.requestId || id;
    }
    if (type === 'constitutional') {
      const row = await db.ConstitutionalChange.findByPk(id, { attributes: ['requestId'] });
      return row?.requestId || id;
    }
    if (type === 'fnf') {
      const row = await db.FnF.findByPk(id, { attributes: ['settlementId'] });
      return row?.settlementId || id;
    }
    return id;
  }

  private static async processRepeatOverdueReminder(track: any, now: Date) {
    // Fast mode compresses TAT for breach/escalation testing — do not spam repeat emails every minute
    if (
      process.env.DEBUG_SLA_FAST_MODE === 'true' &&
      process.env.DEBUG_SLA_REPEAT_IN_FAST_MODE !== 'true'
    ) {
      return;
    }

    let repeatMs = 24 * 60 * 60 * 1000;
    if (process.env.DEBUG_SLA_FAST_MODE === 'true') {
      repeatMs = 60 * 1000;
    }

    const thresholdKey = SlaDispatchKeys.repeatOverdueBucket(now.getTime(), repeatMs);
    const claimed = await claimSlaNotificationDispatch({
      trackingId: track.id,
      thresholdKey,
      dispatchType: 'repeat_overdue',
      templateCode: 'SLA_REMINDER',
      stageName: track.stageName
    });
    if (!claimed) return;

    const caseLabel = await this.getCaseLabel(track);
    const recipientCount = await this.notifyStakeholder(track, 'SLA_REMINDER', {
      title: `SLA Still Overdue: ${track.stageName}`,
      message: `Case ${caseLabel} remains overdue for ${track.stageName}. Please close or escalate.`,
      slaDispatchKey: `${track.id}:${thresholdKey}`
    });
    await updateSlaDispatchRecipientCount(track.id, thresholdKey, recipientCount);
    await this.logSlaActivity(track, `[SLA] Repeat overdue reminder: ${track.stageName} still open.`);
  }

  private static async processReminders(
    track: any,
    config: any,
    now: Date,
    deadline: Date,
    tatMs: number,
    elapsedMs: number
  ) {
    const msRemaining = Math.max(0, tatMs - elapsedMs);
    const caseLabel = await this.getCaseLabel(track);

    for (const reminder of config.reminders || []) {
      if (reminder.isEnabled === false) continue;

      const reminderMs = this.getTatInMs(reminder.timeValue, reminder.timeUnit);

      if (msRemaining <= reminderMs) {
        const thresholdKey = SlaDispatchKeys.preBreachReminder(reminder);
        const legacyKey = this.reminderSentKey(reminder);
        if (await wasSlaNotificationDispatched(track.id, thresholdKey, track.metadata, legacyKey)) {
          continue;
        }

        const claimed = await claimSlaNotificationDispatch({
          trackingId: track.id,
          thresholdKey,
          dispatchType: 'pre_breach_reminder',
          templateCode: 'SLA_REMINDER',
          stageName: track.stageName,
          reminderId: reminder.id ?? null
        });
        if (!claimed) continue;

        const timeStr = `${reminder.timeValue} ${reminder.timeUnit}`;
        console.log(
          `[SLA Service] PRE_BREACH reminder (once): ${track.stageName} — ${timeStr} remaining [${thresholdKey}]`
        );

        const recipientCount = await this.notifyStakeholder(track, 'SLA_REMINDER', {
          title: `SLA Reminder: ${track.stageName}`,
          message: `Case ${caseLabel} is approaching its SLA deadline for ${track.stageName}.`,
          slaDispatchKey: `${track.id}:${thresholdKey}`
        });
        await updateSlaDispatchRecipientCount(track.id, thresholdKey, recipientCount);

        await this.logSlaActivity(
          track,
          `[SLA] Reminder sent: ${track.stageName} has ${timeStr} remaining.`
        );
      }
    }
  }

  private static async triggerBreach(track: any, now: Date) {
    const thresholdKey = SlaDispatchKeys.breach();
    if (await wasSlaNotificationDispatched(track.id, thresholdKey, track.metadata, 'breach_notified')) {
      return;
    }

    const claimed = await claimSlaNotificationDispatch({
      trackingId: track.id,
      thresholdKey,
      dispatchType: 'breach',
      templateCode: 'SLA_BREACH',
      stageName: track.stageName
    });
    if (!claimed) return;

    const caseLabel = await this.getCaseLabel(track);
    console.log(`[SLA Service] BREACHED (once): ${track.stageName} — ${caseLabel}`);
    await track.update({ isBreached: true });
    track.isBreached = true;

    await SLABreach.create({
      trackingId: track.id,
      breachedAt: now,
      status: 'Open'
    });

    const recipientCount = await this.notifyStakeholder(track, 'SLA_BREACH', {
      title: `SLA BREACHED: ${track.stageName}`,
      message: `Case ${caseLabel} has breached its SLA for ${track.stageName}.`,
      slaDispatchKey: `${track.id}:${thresholdKey}`
    });
    await updateSlaDispatchRecipientCount(track.id, thresholdKey, recipientCount);

    await this.logSlaActivity(
      track,
      `[SLA] BREACH: ${track.stageName} has exceeded its Turnaround Time (TAT).`
    );
  }

  private static async processEscalations(track: any, config: any, now: Date, deadline: Date) {
    const msSinceBreach = now.getTime() - deadline.getTime();
    const caseLabel = await this.getCaseLabel(track);

    for (const esc of config.escalationConfigs || []) {
      const escMs = this.getTatInMs(esc.timeValue, esc.timeUnit);

      if (msSinceBreach >= escMs) {
        const thresholdKey = SlaDispatchKeys.escalation(esc.level);
        const legacyKey = this.escalationSentKey(esc.level);
        if (await wasSlaNotificationDispatched(track.id, thresholdKey, track.metadata, legacyKey)) {
          continue;
        }

        const claimed = await claimSlaNotificationDispatch({
          trackingId: track.id,
          thresholdKey,
          dispatchType: 'escalation',
          templateCode: 'SLA_ESCALATION',
          stageName: track.stageName,
          escalationLevel: esc.level
        });
        if (!claimed) continue;

        console.log(
          `[SLA Service] ESCALATION (once) L${esc.level}: ${track.stageName} (Role: ${esc.notifyRole || 'N/A'}) [${thresholdKey}]`
        );

        let recipientCount = 0;
        const recipientIds: string[] = [];
        if (esc.notifyRole) {
          recipientIds.push(
            ...(await resolveRecipientsForRoles(
              {
                entityType: track.entityType,
                entityId: track.entityId,
                applicationId: track.applicationId
              },
              [esc.notifyRole]
            ))
          );
        }

        if (recipientIds.length === 0 && esc.notifyEmail) {
          recipientCount += 1;
          await NotificationService.notify(null, esc.notifyEmail, {
            title: `SLA ESCALATION [L${esc.level}]: ${track.stageName}`,
            message: `Case ${caseLabel} remains incomplete after SLA breach for ${track.stageName}.`,
            channels: ['email', 'system'],
            templateCode: 'SLA_ESCALATION',
            placeholders: {
              applicationId: caseLabel,
              stageName: track.stageName,
              level: esc.level,
              timeValue: esc.timeValue,
              timeUnit: esc.timeUnit,
              phone: ''
            },
            metadata: {
              entityType: track.entityType,
              entityId: track.entityId,
              slaDispatchKey: `${track.id}:${thresholdKey}`
            }
          });
        }

        for (const recipientId of recipientIds) {
          const user = await User.findByPk(recipientId);
          if (!user?.email) continue;
          recipientCount += 1;
          const phone = user.mobileNumber || user.phone;
          await NotificationService.notify(recipientId, user.email, {
            title: `SLA ESCALATION [L${esc.level}]: ${track.stageName}`,
            message: `Case ${caseLabel} remains incomplete after SLA breach for ${track.stageName}.`,
            channels: phone ? ['email', 'system', 'whatsapp'] : ['email', 'system'],
            templateCode: 'SLA_ESCALATION',
            placeholders: {
              applicationId: caseLabel,
              stageName: track.stageName,
              level: esc.level,
              timeValue: esc.timeValue,
              timeUnit: esc.timeUnit,
              phone: phone || ''
            },
            metadata: {
              entityType: track.entityType,
              entityId: track.entityId,
              slaDispatchKey: `${track.id}:${thresholdKey}`
            }
          });
        }

        await updateSlaDispatchRecipientCount(track.id, thresholdKey, recipientCount);

        await this.logSlaActivity(
          track,
          `[SLA] ESCALATION Level ${esc.level}: Escalated to ${esc.notifyRole || esc.notifyEmail || 'supervisor'} for stage ${track.stageName}.`
        );
      }
    }
  }

  private static async logSlaActivity(track: any, text: string) {
    const admin =
      (await User.findOne({ where: { roleCode: 'Super Admin', status: 'active' } })) ||
      (await User.findOne());

    if (!admin) return;

    if (track.entityType === 'application' && track.applicationId) {
      try {
        await db.Worknote.create({
          applicationId: track.applicationId,
          userId: admin.id,
          noteText: text,
          noteType: 'system',
          status: 'active'
        });
      } catch (err) {
        console.error('[SLA Service] Failed to log application work note:', err);
      }
      return;
    }

    const requestTypeMap: Record<string, WorkflowActivityRequestType | undefined> = {
      termination: 'termination',
      resignation: 'resignation',
      relocation: 'relocation',
      constitutional: 'constitutional',
      fnf: 'fnf'
    };
    const requestType = requestTypeMap[track.entityType];
    if (!requestType) return;

    try {
      await writeWorkflowActivityWorknote({
        requestId: track.entityId,
        requestType,
        userId: admin.id,
        noteText: text,
        noteType: 'workflow'
      });
    } catch (err) {
      console.error('[SLA Service] Failed to log offboarding work note:', err);
    }
  }

  private static linkForEntity(entityType: string, entityId: string): string {
    const base = getFrontendBaseUrl();
    switch (entityType) {
      case 'application':
        return `${base}/applications/${entityId}`;
      case 'termination':
        return `${base}/termination/${entityId}`;
      case 'resignation':
        return `${base}/resignation/${entityId}`;
      case 'relocation':
        return `${base}/relocation/${entityId}`;
      case 'constitutional':
        return `${base}/constitutional-change/${entityId}`;
      case 'fnf':
        return `${base}/fnf/${entityId}`;
      default:
        return base;
    }
  }

  private static async notifyStakeholder(
    track: any,
    template: string,
    content: { title: string; message: string; slaDispatchKey?: string }
  ): Promise<number> {
    const config = await this.findConfigForStage(track.stageName);
    if (!config?.ownerRole) return 0;

    const roles = config.ownerRole.split(',').map((r: string) => r.trim());
    const recipientIds = await resolveRecipientsForRoles(
      {
        entityType: track.entityType,
        entityId: track.entityId,
        applicationId: track.applicationId
      },
      roles
    );
    if (recipientIds.length === 0) return 0;

    const caseLabel = await this.getCaseLabel(track);
    const link = this.linkForEntity(track.entityType, track.entityId);
    let sent = 0;

    for (const userId of recipientIds) {
      const user = await User.findByPk(userId);
      if (!user?.email) continue;
      sent += 1;
      const phone = user.mobileNumber || user.phone || null;
      await NotificationService.notify(userId, user.email, {
        title: content.title,
        message: content.message,
        channels: phone ? ['email', 'system', 'whatsapp'] : ['email', 'system'],
        templateCode: template,
        placeholders: {
          applicationId: caseLabel,
          stageName: track.stageName,
          link,
          phone: phone || ''
        },
        metadata: {
          entityType: track.entityType,
          entityId: track.entityId,
          applicationId: track.applicationId,
          slaDispatchKey: content.slaDispatchKey || `${track.id}:${template}`
        }
      });
    }
    return sent;
  }
}
