/**
 * Exception Service
 *
 * Applies translation exceptions — strings that should never be translated.
 * Exceptions are supplied per request by the client that owns them; the API
 * stores none of them. Uses placeholder substitution (__EXCPT_N__) before
 * sending to Gemini, then restores originals after translation.
 */

import { z } from 'zod';
import { logger } from '../utils/logger';

export const MAX_EXCEPTION_TEXT_LENGTH = 500;
export const MAX_EXCEPTIONS_PER_REQUEST = 5000;

const PLACEHOLDER_PATTERN = /__TAG_\d+__|__EXCPT_\d+__/;

const WORD_CHAR = '[\\p{L}\\p{N}\\p{M}]';

export const ExceptionRuleSchema = z.object({
  text: z
    .string()
    .trim()
    .min(1, 'Exception text cannot be empty')
    .max(MAX_EXCEPTION_TEXT_LENGTH, `Exception text cannot exceed ${MAX_EXCEPTION_TEXT_LENGTH} characters`)
    .refine(
      (text) => !PLACEHOLDER_PATTERN.test(text),
      'Exception text cannot contain placeholder patterns (__TAG_N__ or __EXCPT_N__)'
    ),
  match_type: z.enum(['exact', 'contains']).default('exact'),
});

export const ExceptionRulesSchema = z
  .array(ExceptionRuleSchema)
  .max(MAX_EXCEPTIONS_PER_REQUEST, `At most ${MAX_EXCEPTIONS_PER_REQUEST} exceptions per request`)
  .optional();

export type ExceptionRule = z.infer<typeof ExceptionRuleSchema>;

export interface ExceptionReplacements {
  processedText: string;
  replacements: Map<string, string>; // placeholder → original exception text
  matchedExceptions: string[];
}

class ExceptionService {
  /**
   * Coerce an untrusted list into valid rules, dropping entries that fail validation.
   * Queue payloads are re-read after a round trip through Redis, so they are
   * revalidated here rather than trusted.
   */
  normalizeRules(input: unknown): ExceptionRule[] {
    if (!Array.isArray(input)) {
      return [];
    }

    const rules: ExceptionRule[] = [];
    for (const candidate of input.slice(0, MAX_EXCEPTIONS_PER_REQUEST)) {
      const parsed = ExceptionRuleSchema.safeParse(candidate);
      if (parsed.success) {
        rules.push(parsed.data);
      }
    }
    return rules;
  }

  /**
   * Find exception strings in source text and replace with __EXCPT_N__ placeholders.
   * Runs BEFORE extractHtmlTags() in the translation pipeline.
   */
  replaceExceptions(rules: ExceptionRule[], sourceText: string): ExceptionReplacements {
    if (!rules || rules.length === 0) {
      return {
        processedText: sourceText,
        replacements: new Map(),
        matchedExceptions: [],
      };
    }

    // Sort by length DESC to prevent partial matches (e.g., "Press.Zone News" before "Press.Zone")
    const sorted = [...rules].sort((a, b) => b.text.length - a.text.length);

    let processedText = sourceText;
    const replacements = new Map<string, string>();
    const matchedExceptions: string[] = [];
    let counter = 0;

    for (const rule of sorted) {
      const escapedText = rule.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      let regex: RegExp;

      if (rule.match_type === 'contains') {
        // Any word containing the text — the whole word is protected, not just
        // the matched part. The surrounding class excludes "_" so an already
        // inserted __EXCPT_N__ placeholder is never swallowed.
        regex = new RegExp(`${WORD_CHAR}*${escapedText}${WORD_CHAR}*`, 'giu');
      } else {
        // Exact match with word boundaries — case-insensitive
        // Use lookahead/lookbehind for word boundaries that work with non-ASCII
        regex = new RegExp(`(?<![\\w])${escapedText}(?![\\w])`, 'gi');
      }

      let matched = false;
      processedText = processedText.replace(regex, (match) => {
        let placeholder: string;
        do {
          placeholder = `__EXCPT_${counter++}__`;
        } while (sourceText.includes(placeholder) || replacements.has(placeholder));
        replacements.set(placeholder, match); // Preserve original casing
        matched = true;
        return placeholder;
      });

      if (matched) {
        matchedExceptions.push(rule.text);
      }
    }

    if (matchedExceptions.length > 0) {
      logger.info('Exception placeholders applied', {
        matchedCount: matchedExceptions.length,
        placeholderCount: counter,
      });
    }

    return { processedText, replacements, matchedExceptions };
  }

  /**
   * Restore __EXCPT_N__ placeholders with original exception text.
   * Runs AFTER restoreHtmlTags() in the translation pipeline.
   */
  restoreExceptions(translatedText: string, replacements: Map<string, string>): string {
    let result = translatedText;

    replacements.forEach((original, placeholder) => {
      // Replace all occurrences (Gemini might have duplicated a placeholder)
      result = result.split(placeholder).join(original);
    });

    return result;
  }
}

// Export singleton instance
export const exceptionService = new ExceptionService();
