/**
 * Phase 8 — Template management service.
 * - Allowlisted variable substitution only (no eval, no expression language).
 * - Unsafe HTML/JS rejection on save/activate.
 * - Immutable versioned snapshots.
 * - Bilingual fallback to English when configured.
 */
import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import {
  CommLanguage,
  CommChannel,
  CommEventType,
  CommTemplateStatus,
  Prisma,
} from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
  ALLOWED_TEMPLATE_VARS,
  TEMPLATE_VAR_PATTERN,
  UNSAFE_HTML_PATTERNS,
} from './communications.constants';

export interface TemplateVariables {
  customerName?: string;
  personName?: string;
  orderNumber?: string;
  trialDate?: string;
  trialTime?: string;
  readyDate?: string;
  invoiceNumber?: string;
  invoiceDate?: string;
  netAmount?: string;
  receiptNumber?: string;
  receiptDate?: string;
  paymentAmount?: string;
  currentOutstanding?: string;
  deliveryDate?: string;
  businessName?: string;
  businessContact?: string;
  businessAddress?: string;
  currency?: string;
  expectedDate?: string;
  orderDate?: string;
  [key: string]: string | undefined;
}

/** Reject unsafe HTML/JS. */
function assertSafeBody(body: string): void {
  for (const pat of UNSAFE_HTML_PATTERNS) {
    if (pat.test(body)) {
      throw new BadRequestException(
        'Template body contains unsafe HTML or JavaScript. Remove script tags, event handlers, or JavaScript expressions.',
      );
    }
  }
}

/** Extract variable names from a template body. */
function extractVars(body: string): string[] {
  const found = new Set<string>();
  let m: RegExpExecArray | null;
  const pat = new RegExp(TEMPLATE_VAR_PATTERN.source, 'g');
  while ((m = pat.exec(body)) !== null) {
    if (m[1]) found.add(m[1]);
  }
  return [...found];
}

/** Validate all variables in body are on the allowlist. */
function assertAllowlistedVars(body: string): void {
  const vars = extractVars(body);
  const invalid = vars.filter((v) => !ALLOWED_TEMPLATE_VARS.has(v));
  if (invalid.length > 0) {
    throw new BadRequestException(
      `Template contains disallowed variables: ${invalid.join(', ')}. Only approved variables are permitted.`,
    );
  }
}

/** Render a template body with given variables. Returns rendered string. */
export function renderTemplate(
  body: string,
  vars: TemplateVariables,
): string {
  return body.replace(new RegExp(TEMPLATE_VAR_PATTERN.source, 'g'), (_match, name: string | undefined) => {
    if (!name) return '';
    const val = vars[name];
    if (val === undefined || val === null) {
      // Return empty string for missing optional vars — never leak broken placeholders
      return '';
    }
    // Escape HTML entities to prevent XSS in email HTML bodies
    return String(val)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  });
}

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

  constructor(private readonly prisma: PrismaService) {}

  // ─── Create template ────────────────────────────────────────────────────────

  async createTemplate(
    organizationId: string,
    input: {
      eventType: CommEventType;
      channel: CommChannel;
      language: CommLanguage;
      name: string;
      subject?: string;
      body: string;
      providerTemplateName?: string;
      providerLanguageCode?: string;
      providerExternalId?: string;
    },
    createdById: string,
  ) {
    assertSafeBody(input.body);
    if (input.subject) assertSafeBody(input.subject);
    assertAllowlistedVars(input.body);
    const usedVars = extractVars(input.body);

    return this.prisma.$transaction(async (tx) => {
      const template = await tx.commTemplate.create({
        data: {
          organizationId,
          eventType: input.eventType,
          channel: input.channel,
          language: input.language,
          name: input.name,
          status: CommTemplateStatus.DRAFT,
          isActive: false,
          providerTemplateName: input.providerTemplateName ?? null,
          providerLanguageCode: input.providerLanguageCode ?? null,
          providerExternalId: input.providerExternalId ?? null,
          createdById,
        },
      });

      const version = await tx.commTemplateVersion.create({
        data: {
          templateId: template.id,
          organizationId,
          version: 1,
          subject: input.subject ?? null,
          body: input.body,
          variables: usedVars as unknown as Prisma.InputJsonValue,
          createdById,
        },
      });

      await tx.commTemplate.update({
        where: { id: template.id },
        data: { currentVersionId: version.id },
      });

      return { ...template, currentVersionId: version.id, versions: [version] };
    });
  }

  // ─── Update template (creates new version, immutable old) ───────────────────

  async updateTemplate(
    organizationId: string,
    templateId: string,
    input: {
      subject?: string;
      body?: string;
      name?: string;
      providerTemplateName?: string;
      providerLanguageCode?: string;
      providerExternalId?: string;
      providerTemplateStatus?: string;
    },
    updatedById: string,
  ) {
    const existing = await this.prisma.commTemplate.findFirst({
      where: { id: templateId, organizationId },
      include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
    });
    if (!existing) throw new NotFoundException('Template not found');

    if (input.body) {
      assertSafeBody(input.body);
      assertAllowlistedVars(input.body);
    }
    if (input.subject) assertSafeBody(input.subject);

    return this.prisma.$transaction(async (tx) => {
      const lastVersion = existing.versions[0]?.version ?? 0;
      let newVersionId = existing.currentVersionId;

      if (input.body !== undefined) {
        const usedVars = extractVars(input.body);
        const newVer = await tx.commTemplateVersion.create({
          data: {
            templateId,
            organizationId,
            version: lastVersion + 1,
            subject: input.subject ?? existing.versions[0]?.subject ?? null,
            body: input.body,
            variables: usedVars as unknown as Prisma.InputJsonValue,
            createdById: updatedById,
          },
        });
        newVersionId = newVer.id;
      }

      return tx.commTemplate.update({
        where: { id: templateId },
        data: {
          name: input.name ?? undefined,
          currentVersionId: newVersionId,
          providerTemplateName: input.providerTemplateName ?? undefined,
          providerLanguageCode: input.providerLanguageCode ?? undefined,
          providerExternalId: input.providerExternalId ?? undefined,
          providerTemplateStatus: input.providerTemplateStatus ?? undefined,
          // Reset to DRAFT if body changed
          status: input.body !== undefined ? CommTemplateStatus.DRAFT : undefined,
          isActive: input.body !== undefined ? false : undefined,
        },
        include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
      });
    });
  }

  // ─── Activate template ───────────────────────────────────────────────────────

  async activateTemplate(
    organizationId: string,
    templateId: string,
    activatedById: string,
  ) {
    const template = await this.prisma.commTemplate.findFirst({
      where: { id: templateId, organizationId },
      include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
    });
    if (!template) throw new NotFoundException('Template not found');
    if (!template.currentVersionId || !template.versions[0]) {
      throw new BadRequestException('Template has no version body. Add content before activating.');
    }

    // Deactivate other active templates for same event/channel/language
    await this.prisma.commTemplate.updateMany({
      where: {
        organizationId,
        eventType: template.eventType,
        channel: template.channel,
        language: template.language,
        isActive: true,
        id: { not: templateId },
      },
      data: { isActive: false, status: CommTemplateStatus.INACTIVE },
    });

    return this.prisma.commTemplate.update({
      where: { id: templateId },
      data: { isActive: true, status: CommTemplateStatus.ACTIVE },
    });
  }

  // ─── List templates ──────────────────────────────────────────────────────────

  async listTemplates(
    organizationId: string,
    filters?: {
      eventType?: CommEventType;
      channel?: CommChannel;
      language?: CommLanguage;
      isActive?: boolean;
    },
  ) {
    return this.prisma.commTemplate.findMany({
      where: {
        organizationId,
        ...(filters?.eventType ? { eventType: filters.eventType } : {}),
        ...(filters?.channel ? { channel: filters.channel } : {}),
        ...(filters?.language ? { language: filters.language } : {}),
        ...(filters?.isActive !== undefined ? { isActive: filters.isActive } : {}),
        status: { not: CommTemplateStatus.ARCHIVED },
      },
      include: {
        versions: { orderBy: { version: 'desc' }, take: 1 },
      },
      orderBy: [{ eventType: 'asc' }, { channel: 'asc' }, { language: 'asc' }],
    });
  }

  // ─── Preview template ────────────────────────────────────────────────────────

  async previewTemplate(
    organizationId: string,
    templateId: string,
    sampleVars: TemplateVariables,
  ): Promise<{ subject: string | null; body: string; isMock: true; label: string }> {
    const template = await this.prisma.commTemplate.findFirst({
      where: { id: templateId, organizationId },
      include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
    });
    if (!template) throw new NotFoundException('Template not found');
    const ver = template.versions[0];
    if (!ver) throw new BadRequestException('Template has no content');

    return {
      subject: ver.subject ? renderTemplate(ver.subject, sampleVars) : null,
      body: renderTemplate(ver.body, sampleVars),
      isMock: true,
      label: 'PREVIEW — NOT SENT',
    };
  }

  // ─── Resolve active template for event/channel/language ─────────────────────

  async resolveTemplate(
    organizationId: string,
    eventType: CommEventType,
    channel: CommChannel,
    language: CommLanguage,
    fallbackToEnglish: boolean,
  ): Promise<{ template: { id: string }; version: { id: string; subject: string | null; body: string } } | null> {
    const tpl = await this.prisma.commTemplate.findFirst({
      where: { organizationId, eventType, channel, language, isActive: true },
      include: {
        versions: {
          where: { id: { not: '' } },
          orderBy: { version: 'desc' },
          take: 1,
        },
      },
    });
    if (tpl && tpl.versions[0]) {
      return { template: tpl, version: tpl.versions[0] };
    }

    // Fallback to English if configured and language was not already EN
    if (language !== 'EN' && fallbackToEnglish) {
      this.logger.warn(
        `No active ${language} template for ${eventType}/${channel} in org ${organizationId}. Falling back to EN.`,
      );
      const enTpl = await this.prisma.commTemplate.findFirst({
        where: { organizationId, eventType, channel, language: 'EN', isActive: true },
        include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
      });
      if (enTpl && enTpl.versions[0]) {
        return { template: enTpl, version: enTpl.versions[0] };
      }
    }

    return null;
  }

  // ─── Get single template ─────────────────────────────────────────────────────

  async getTemplate(organizationId: string, templateId: string) {
    const t = await this.prisma.commTemplate.findFirst({
      where: { id: templateId, organizationId },
      include: { versions: { orderBy: { version: 'asc' } } },
    });
    if (!t) throw new NotFoundException('Template not found');
    return t;
  }
}
