import { Injectable, Logger } from '@nestjs/common';
import { CommunicationTemplatesMaster } from 'src/common/entities/communication-templates-master.entity';
import { CommunicationTemplates } from 'src/common/entities/communication-templates.entity';
import { CommunicationTypeEnum } from 'src/common/enum/communication-type.enum';
import { TemplateUsageTypeEnum } from 'src/common/enum/template-usage-type.enum';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { CommunicationTemplatesMasterRepository } from '../repositories/communication-templates-master.repository';
import { CommunicationTemplatesRepository } from '../repositories/communication-templates.repository';
import { MergeInfoAnswerLocationMapRepository } from '../repositories/merge-info-answer-location-map.repository';
import { CommunicationStepSelection } from '../interfaces/communication-config.interface';

@Injectable()
export class CommunicationTemplatesMasterService {
  private readonly logger = new Logger(CommunicationTemplatesMasterService.name);

  constructor(
    private readonly masterRepo: CommunicationTemplatesMasterRepository,
    private readonly templatesRepo: CommunicationTemplatesRepository,
    private readonly mergeInfoRepo: MergeInfoAnswerLocationMapRepository,
  ) {}

  /**
   * Get all master templates
   */
  async getAllMasterTemplates(limit: number = 50, offset: number = 0): Promise<any> {
    try {
      this.logger.log(`Fetching all master templates with limit: ${limit}, offset: ${offset}`);
      return await this.masterRepo.findAll(limit, offset);
    } catch (error) {
      this.logger.error(`Failed to fetch master templates: ${error.message}`, error.stack);
      handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_GET_FAILED, error);
    }
  }

  /**
   * Get master template by key
   */
  async getMasterTemplateByKey(
    templateKey: string,
  ): Promise<CommunicationTemplatesMaster> {
    try {
      this.logger.log(`Fetching master template with key: ${templateKey}`);
      const template = await this.masterRepo.findByTemplateKey(templateKey);
      if (!template) {
        handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_FIND_BY_KEY_FAILED, new Error('Template not found'));
      }
      return template;
    } catch (error) {
      this.logger.error(
        `Failed to fetch master template by key: ${error.message}`,
        error.stack,
      );
      handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_FIND_BY_KEY_FAILED, error);
    }
  }

  /**
   * Clone master template to program-level template
   * Expands mergeFieldMap JSON into merge_info_answer_location_map rows
   */
  async cloneMasterTemplateToProgram(
    templateKey: string,
    programId: number,
    templateUsageType: TemplateUsageTypeEnum = TemplateUsageTypeEnum.GENERAL,
    category?: string,
    createdBy?: number,
  ): Promise<CommunicationTemplates> {
    try {
      this.logger.log(
        `Cloning master template ${templateKey} to program ${programId}`,
      );
      // Fetch master template
      const masterTemplate = await this.masterRepo.findByTemplateKey(templateKey);
      if (!masterTemplate) {
        handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_FIND_BY_KEY_FAILED, new Error('Template not found'));
      }
      // Use templateId directly from master template
      const templateId = masterTemplate.templateId;
      // Create program-level template
      const programTemplate = await this.templatesRepo.createEntity({
        templateId,
        templateName: masterTemplate.templateName,
        templateKey: masterTemplate.templateKey,
        templateAccessKey: masterTemplate.templateAccessKey,
        templateType: masterTemplate.templateType,
        category: category as any,
        programId,
        templateUsageType,
        sandboxTemplateId: masterTemplate.sandboxTemplateId, 
        displayOrder: 0,
        createdBy,
        updatedBy: createdBy,
      });
      // Expand mergeFieldMap into merge_info_answer_location_map rows
      if (masterTemplate.mergeFieldMap && Array.isArray(masterTemplate.mergeFieldMap)) {
        for (const mergeField of masterTemplate.mergeFieldMap) {
          await this.mergeInfoRepo.createEntity({
            keyName: mergeField.keyName,
            templateId: programTemplate.id,
            isCommon: false,
            sourceTable: mergeField.sourceTable,
            sourceColumn: mergeField.sourceColumn,
            dataType: mergeField.dataType as any,
            formatType: mergeField.formatType,
            isNullable: mergeField.isNullable,
            defaultValue: mergeField.defaultValue,
            description: mergeField.description,
            isActive: true,
            isPerRecipient: mergeField.isPerRecipient || false,
          });
        }
      }
      this.logger.log(
        `Successfully cloned template ${templateKey} to program ${programId} with ID ${programTemplate.id}`,
      );
      return programTemplate;
    } catch (error) {
      this.logger.error(
        `Failed to clone master template ${templateKey} to program ${programId}: ${error.message}`,
        error.stack,
      );
      handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_GET_FAILED, error);
    }
  }

  /**
   * Clone all master templates for a programTypeId to a program in one pass.
   *
   * When `stepSelections` is provided (Add Program create with explicit admin choices),
   * each clone's `attached_steps` is computed up-front by collecting every step the
   * master id appears in. No second update pass is needed.
   *
   * When `stepSelections` is omitted (legacy callers / no FE config), each clone falls
   * back to its master's `is_default` snapshot and `attached_steps = [master.step]`
   * for enabled rows, `[]` for disabled.
   */
  async cloneMultipleMasterTemplatesToProgramByProgramType(
    programTypeId: number,
    programId: number,
    templateUsageType: TemplateUsageTypeEnum = TemplateUsageTypeEnum.GENERAL,
    createdBy?: number,
    stepSelections?: CommunicationStepSelection[],
  ): Promise<CommunicationTemplates[]> {
    try {
      this.logger.log(
        `Cloning all master templates for programTypeId ${programTypeId} to program ${programId}`,
      );
      // Fetch all master templates for the programTypeId and usage type
      const masterTemplates = await this.masterRepo.findAllByProgramTypeId(programTypeId, templateUsageType);
      if (!masterTemplates.length) {
        this.logger.warn(`No master templates found for programTypeId ${programTypeId}`);
        return [];
      }
      // Build a map masterId -> Set(step) from the admin's per-step selection so each
      // master's attached_steps can be resolved in O(1) below. When stepSelections is
      // absent, the map stays empty and we fall back to master.is_default.
      const hasExplicitSelection = Array.isArray(stepSelections);
      const stepsByMaster = new Map<number, Set<string>>();
      for (const selection of stepSelections ?? []) {
        for (const masterTemplateId of selection.enabledMasterTemplateIds ?? []) {
          const key = Number(masterTemplateId);
          if (!stepsByMaster.has(key)) {
            stepsByMaster.set(key, new Set());
          }
          stepsByMaster.get(key)!.add(selection.step);
        }
      }
      const clonedTemplates: CommunicationTemplates[] = [];
      for (const masterTemplate of masterTemplates) {
        try {
          let attachedSteps: string[];
          let isEnabled: boolean;
          if (hasExplicitSelection) {
            attachedSteps = Array.from(stepsByMaster.get(Number(masterTemplate.id)) ?? []);
            isEnabled = attachedSteps.length > 0;
          } else {
            isEnabled = masterTemplate.isDefault;
            attachedSteps =
              isEnabled && masterTemplate.step
                ? [masterTemplate.step as unknown as string]
                : [];
          }
          // Create program-level template
          const programTemplate = await this.templatesRepo.createEntity({
            templateId: masterTemplate.templateId,
            templateName: masterTemplate.templateName,
            templateKey: masterTemplate.templateKey,
            templateAccessKey: masterTemplate.templateAccessKey,
            templateType: masterTemplate.templateType,
            category: masterTemplate.category,
            programId,
            templateUsageType,
            sandboxTemplateId: masterTemplate.sandboxTemplateId,
            displayOrder: 0,
            masterTemplateId: masterTemplate.id,
            isEnabled,
            step: masterTemplate.step,
            attachedSteps,
            createdBy,
            updatedBy: createdBy,
          });
          // Expand mergeFieldMap into merge_info_answer_location_map rows
          if (masterTemplate.mergeFieldMap && Array.isArray(masterTemplate.mergeFieldMap)) {
            for (const mergeField of masterTemplate.mergeFieldMap) {
              await this.mergeInfoRepo.createEntity({
                keyName: mergeField.keyName,
                templateId: programTemplate.id,
                isCommon: false,
                sourceTable: mergeField.sourceTable,
                sourceColumn: mergeField.sourceColumn,
                dataType: mergeField.dataType as any,
                formatType: mergeField.formatType,
                isNullable: mergeField.isNullable,
                defaultValue: mergeField.defaultValue,
                description: mergeField.description,
                isActive: true,
                isPerRecipient: mergeField.isPerRecipient || false,
              });
            }
          }
          clonedTemplates.push(programTemplate);
        } catch (error) {
          this.logger.warn(
            `Failed to clone template ${masterTemplate.templateKey} to program ${programId}: ${error.message}`,
          );
          // Continue with other templates
        }
      }
      this.logger.log(
        `Successfully cloned ${clonedTemplates.length} out of ${masterTemplates.length} templates to program ${programId}`,
      );
      return clonedTemplates;
    } catch (error) {
      this.logger.error(
        `Failed to clone multiple master templates to program ${programId}: ${error.message}`,
        error.stack,
      );
      handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_GET_FAILED, error);
    }
  }

  /**
   * Validate that every supplied master template id references an active master row
   * whose program_type_id matches. Throws InifniBadRequestException (400) listing the
   * offending ids in `details` when any are invalid.
   */
  async validateMasterTemplateIdsForProgramType(
    programTypeId: number,
    masterTemplateIds: number[],
  ): Promise<void> {
    const invalidIds = await this.masterRepo.findInvalidMasterIdsForProgramType(
      programTypeId,
      masterTemplateIds,
    );
    if (invalidIds.length > 0) {
      throw new InifniBadRequestException(
        ERROR_CODES.INVALID_MASTER_TEMPLATE_IDS,
        null,
        { invalidIds },
      );
    }
  }

  /**
   * Rev 10 — validate a per-step selection against the program type. Every `step` must be a
   * real step of the type and every id must be an active master of the type. Throws
   * InifniBadRequestException (400) listing offending ids/steps in `details`.
   */
  async validatePerStepSelectionForProgramType(
    programTypeId: number,
    steps: CommunicationStepSelection[],
  ): Promise<void> {
    if (!steps || steps.length === 0) {
      return;
    }
    const masters = await this.masterRepo.findActiveSummariesByProgramTypeId(programTypeId);
    const validStepSet = new Set(
      masters
        .map((master) => master.step)
        .filter((step): step is any => !!step)
        .map((step) => String(step)),
    );
    const validIdSet = new Set(masters.map((master) => Number(master.id)));

    const invalidSteps = new Set<string>();
    const invalidIds = new Set<number>();
    for (const sel of steps) {
      if (!validStepSet.has(String(sel.step))) {
        invalidSteps.add(sel.step);
      }
      for (const id of sel.enabledMasterTemplateIds ?? []) {
        if (!validIdSet.has(Number(id))) {
          invalidIds.add(Number(id));
        }
      }
    }
    if (invalidSteps.size > 0 || invalidIds.size > 0) {
      throw new InifniBadRequestException(ERROR_CODES.INVALID_MASTER_TEMPLATE_IDS, null, {
        invalidIds: Array.from(invalidIds),
        invalidSteps: Array.from(invalidSteps),
      });
    }
  }

  /**
   * Apply the admin's per-step template selection to an existing program's clones.
   * Updates each clone's attached_steps array; soft-deletes clones with no remaining
   * attachments. Used by the Add Program edit flow (PUT /v1/programs).
   */
  async applyPerStepSelectionForProgram(
    programId: number,
    perStep: CommunicationStepSelection[],
    updatedBy?: number,
  ): Promise<void> {
    await this.templatesRepo.applyPerStepSelectionForProgram(programId, perStep, updatedBy);
  }

  /**
   * Get program-level template by programId and templateKey
   * Fetches from DB, not from environment variables
   */
  async getProgramTemplate(
    programId: number,
    templateKey: string,
    templateType?: CommunicationTypeEnum,
  ): Promise<CommunicationTemplates> {
    try {
      this.logger.log(
        `Fetching program template for program ${programId} with key ${templateKey}`,
      );
      let template: CommunicationTemplates | null;
      if (templateType) {
        template = await this.templatesRepo.findByProgramAndKeyAndType(
          programId,
          templateKey,
          templateType,
        );
      } else {
        template = await this.templatesRepo.findByProgramAndKey(programId, templateKey);
      }
      if (!template) {
        this.logger.warn(
          `Template not found for program ${programId} with key ${templateKey}. Consider cloning from master.`,
        );
        handleKnownErrors(
          ERROR_CODES.COMMUNICATION_TEMPLATE_FIND_BY_KEY_FAILED,
          new Error(`Template not found for programId: ${programId}, templateKey: ${templateKey}`),
        );
      }
      return template;
    } catch (error) {
      this.logger.error(
        `Failed to fetch program template: ${error.message}`,
        error.stack,
      );
      handleKnownErrors(ERROR_CODES.COMMUNICATION_TEMPLATE_FIND_BY_KEY_FAILED, error);
    }
  }

  /**
   * Get template with merge info mappings
   */
  async getTemplateWithMergeInfo(
    programId: number,
    templateKey: string,
  ): Promise<any> {
    try {
      const template = await this.getProgramTemplate(programId, templateKey);
      return await this.mergeInfoRepo.getTemplateWithMergeInfo(template);
    } catch (error) {
      this.logger.error(
        `Failed to fetch template with merge info: ${error.message}`,
        error.stack,
      );
      handleKnownErrors(ERROR_CODES.MERGE_INFO_NOT_FOUND, error);
    }
  }
}
