import { type Theme } from '@material-hu/mui/styles';

import { type FileCardType } from '@material-hu/components/design-system/FileCard/types';

import { resolveFileCardSrc } from 'src/utils/filesUtils';
import { type TCustomFunction } from 'src/utils/i18n';

import { CertificateStatus, CertificateType } from '../constants';
import {
  type CertificateFileAsset,
  type CertificateSignature,
  type CreateLearningCertificateRequest,
  type LearningCertificateColor,
  type UpdateLearningCertificateRequest,
} from '../types';

import {
  type CertificateLogoDisplay,
  type CertificateSignatureDisplay,
} from './components/CertificatePreviewCanvas/types';
import {
  CERTIFICATE_FIELD_I18N_BASE,
  CERTIFICATE_FIELD_I18N_KEY,
  CERTIFICATE_MAX_LOGOS,
  CERTIFICATE_MAX_SIGNATURES,
  type CertificateAdditionalFieldType,
  certificateAdditionalFieldTypes,
} from './constants';
import {
  type CertificateCreateFormValues,
  type LogoSlot,
  type SignatureSlot,
} from './types';

export const getActiveSignatures = (
  signaturesEnabled: boolean,
  signatures: CertificateSignatureDisplay[],
): CertificateSignatureDisplay[] =>
  signaturesEnabled ? signatures.filter(s => s.imageUrl) : [];

export const getActiveLogos = (
  logosEnabled: boolean,
  logos: CertificateLogoDisplay[],
): CertificateLogoDisplay[] =>
  logosEnabled ? logos.filter(l => l.imageUrl) : [];

/** A persisted signature paired with its file asset, joined by `fileAssetId`. */
export type CertificateSignatureWithAsset = {
  signature: CertificateSignature;
  asset: CertificateFileAsset | undefined;
};

/**
 * Joins each persisted signature with its file asset by `fileAssetId`. Shared by
 * the list preview (asset → display URL) and the edit form (asset → FileCard) so
 * the signature/asset pairing lives in a single place.
 */
export const joinSignaturesWithAssets = (
  signatures: CertificateSignature[] | null | undefined,
  fileAssets: CertificateFileAsset[] | null | undefined,
): CertificateSignatureWithAsset[] => {
  const assetById = new Map((fileAssets ?? []).map(asset => [asset.id, asset]));
  return (signatures ?? []).map(signature => ({
    signature,
    asset: assetById.get(signature.fileAssetId),
  }));
};

/** Maps form signature slots to display data for the certificate preview. */
export const mapSignatureSlots = (
  slots: SignatureSlot[] | undefined,
): CertificateSignatureDisplay[] =>
  (slots ?? []).map(slot => ({
    imageUrl: resolveFileCardSrc(slot.file[0]),
    primaryText: slot.signerName,
    secondaryText: slot.signerTitle,
  }));

/** Maps form logo slots to display data for the certificate preview. */
export const mapLogoSlots = (
  slots: LogoSlot[] | undefined,
): CertificateLogoDisplay[] =>
  (slots ?? []).map(slot => ({
    imageUrl: resolveFileCardSrc(slot.file[0]),
  }));

/** Resolves the certificate accent color to a hex value from the theme. */
export const getCertificateAccentHex = (
  accentColor: LearningCertificateColor,
  theme: Theme,
  instanceColor: string | null | undefined,
): string => {
  const { palette } = theme;
  const newBase = palette.newBase;

  const accentHexMap: Record<LearningCertificateColor, string | undefined> = {
    COMMUNITY: instanceColor ?? palette.new.graphics.brand[500],
    LIGHT_BLUE: palette.new.graphics.brand[300],
    DARK_BLUE: palette.new.graphics.brand[700],
    LIGHT_BROWN: palette.new.graphics.tan[500],
    DARK_BROWN: palette.new.graphics.tan[600],
    LIGHT_GREEN: palette.new.graphics.lime[500],
    DARK_GREEN: palette.new.graphics.lime[600],
    LIGHT_VIOLET: palette.new.graphics.purple[500],
    DARK_VIOLET: palette.new.graphics.purple[600],
    LIGHT_PURPLE: palette.new.graphics.mulberry[500],
    DARK_PURPLE: palette.new.graphics.mulberry[700],
    LIGHT_SKY_BLUE: palette.new.graphics.info[500],
    DARK_SKY_BLUE: palette.new.graphics.info[600],
    LIGHT_GRAY: newBase?.grey[100] ?? palette.grey[100],
    DARK_GRAY: newBase?.grey[950] ?? palette.grey[900],
    WHITE: newBase?.white ?? palette.common.white,
  };

  return accentHexMap[accentColor] ?? palette.new.graphics.brand[300];
};

export const isValidSignatureSlot = (slot: SignatureSlot): boolean =>
  slot.file.length > 0 || !!slot.signerName || !!slot.signerTitle;

export const isValidLogoSlot = (slot: LogoSlot): boolean =>
  slot.file.length > 0;

/** Extracts the uploaded file asset id from a single-file uploader field. */
const getFileAssetId = (files: FileCardType[]): number | undefined =>
  files[0]?.fileAsset?.id;

/** True when the single-file background uploader holds a selected or uploaded file. */
export const hasBackgroundFile = (
  files: FileCardType[] | undefined,
): boolean => {
  const backgroundCard = files?.[0];
  if (!backgroundCard || backgroundCard.status === 'uploading') return false;
  return Boolean(backgroundCard.file || backgroundCard.fileAsset);
};

/** Shared basic-information fields used by both the create and update bodies. */
const formatBasicInformationFields = (
  values: CertificateCreateFormValues,
  t: TCustomFunction,
) => {
  const pictureId =
    values.type === CertificateType.CUSTOM_BACKGROUND
      ? getFileAssetId(values.certificateCustomBackgroundFile)
      : undefined;

  const subtitle = values.subtitle
    ? convertFieldTokensToBackend(values.subtitle, t)
    : undefined;
  const description = values.description
    ? convertFieldTokensToBackend(values.description, t)
    : undefined;

  return {
    name: values.name,
    type: values.type,
    title: values.title,
    subtitle,
    description,
    pictureId,
  };
};

/**
 * Builds the `POST /learning-certificates` body from the form (step 2).
 * Creates the certificate as an inactive draft; design fields (color,
 * alignment, signatures, logos) are sent later via PATCH.
 */
export const formatCreateRequest = (
  values: CertificateCreateFormValues,
  t: TCustomFunction,
): CreateLearningCertificateRequest => ({
  ...formatBasicInformationFields(values, t),
  status: CertificateStatus.INACTIVE,
});

/**
 * Builds the basic-information `PATCH` body. Unlike `formatCreateRequest` it
 * never sends `status`: the status is owned by creation (POST draft) and final
 * activation only, so editing the basic info must not flip an active
 * certificate back to inactive.
 *
 * `subtitle` and `description` are sent as explicit `null` (not `undefined`)
 * when cleared, because a PATCH ignores `undefined` and would otherwise never
 * persist their removal.
 */
export const formatBasicInformationUpdateRequest = (
  values: CertificateCreateFormValues,
  t: TCustomFunction,
): UpdateLearningCertificateRequest => ({
  ...formatBasicInformationFields(values, t),
  subtitle: values.subtitle
    ? convertFieldTokensToBackend(values.subtitle, t)
    : null,
  description: values.description
    ? convertFieldTokensToBackend(values.description, t)
    : null,
});

/**
 * Builds the `PATCH /learning-certificates/:id` body for the design step
 * (step 3): color, alignment and the uploaded signatures and logos.
 */
export const formatDesignUpdateRequest = (
  values: CertificateCreateFormValues,
): UpdateLearningCertificateRequest => {
  const signatures = values.certificateSignaturesEnabled
    ? values.signatureSlots.flatMap(slot => {
        const fileAssetId = getFileAssetId(slot.file);
        if (!fileAssetId) return [];
        return [
          {
            fileAssetId,
            signerName: slot.signerName || undefined,
            signerTitle: slot.signerTitle || undefined,
          },
        ];
      })
    : [];

  const logoFileAssetIds = values.certificateLogosEnabled
    ? values.logoSlots.flatMap(slot => {
        const fileAssetId = getFileAssetId(slot.file);
        return fileAssetId ? [fileAssetId] : [];
      })
    : [];

  return {
    color: values.color,
    alignment: values.alignment,
    signatures,
    logoFileAssetIds,
  };
};

export const getValidSignatureSlotsCount = (slots: SignatureSlot[]): number =>
  slots.filter(isValidSignatureSlot).length;

export const getValidLogoSlotsCount = (slots: LogoSlot[]): number =>
  slots.filter(isValidLogoSlot).length;

export const isMaxSignatureSlots = (count: number): boolean =>
  count >= CERTIFICATE_MAX_SIGNATURES;

export const isMaxLogoSlots = (count: number): boolean =>
  count >= CERTIFICATE_MAX_LOGOS;

export function getCertificateFieldDisplayLabel(
  translate: (key: string) => string,
  type: CertificateAdditionalFieldType,
): string {
  return translate(
    `${CERTIFICATE_FIELD_I18N_BASE}.${CERTIFICATE_FIELD_I18N_KEY[type]}`,
  );
}

export function getCertificateFieldExampleLabel(
  translate: (key: string) => string,
  type: CertificateAdditionalFieldType,
): string {
  return translate(
    `${CERTIFICATE_FIELD_I18N_BASE}.${CERTIFICATE_FIELD_I18N_KEY[type]}_example`,
  );
}

export function getCertificateFieldToken(
  translate: (key: string) => string,
  type: CertificateAdditionalFieldType,
): string {
  return `{${getCertificateFieldDisplayLabel(translate, type)}}`;
}

/**
 * Converts the localized field tokens embedded in a text (e.g. `{Fecha de inicio}`)
 * into the backend placeholder format (`{{startDate}}`). Replacements run from the
 * longest label to the shortest to avoid partial matches; anything that doesn't
 * match a known field is left untouched, mirroring the backend substitution.
 */
export function convertFieldTokensToBackend(
  text: string,
  translate: (key: string) => string,
): string {
  const replacements = certificateAdditionalFieldTypes
    .map(type => ({
      localized: getCertificateFieldToken(translate, type),
      token: `{{${type}}}`,
    }))
    .sort((a, b) => b.localized.length - a.localized.length);

  let result = text;
  for (const { localized, token } of replacements) {
    result = result.replaceAll(localized, token);
  }
  return result;
}

/**
 * Inverse of `convertFieldTokensToBackend`: converts the backend placeholders
 * embedded in a text (`{{startDate}}`) back into the localized field tokens
 * (`{Fecha de inicio}`) for the known field types. Unknown placeholders such as
 * `{{fullname}}` are left untouched so the editor renders them verbatim.
 */
export function convertFieldTokensToLocalized(
  text: string,
  translate: (key: string) => string,
): string {
  const replacements = certificateAdditionalFieldTypes
    .map(type => ({
      token: `{{${type}}}`,
      localized: getCertificateFieldToken(translate, type),
    }))
    .sort((a, b) => b.token.length - a.token.length);

  let result = text;
  for (const { token, localized } of replacements) {
    result = result.replaceAll(token, localized);
  }
  return result;
}

export function getCertificateFieldTokenLegacy(
  type: CertificateAdditionalFieldType,
): string {
  return `{${type}}`;
}

function escapeRegExp(text: string): string {
  return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}

export function stripCurlyBraceSegments(text: string): string {
  return text.replace(/\{[^}]*\}/g, '');
}

function buildTokenSplitRegex(translate: (key: string) => string): RegExp {
  const patterns: string[] = [];
  for (const type of certificateAdditionalFieldTypes) {
    const localized = getCertificateFieldToken(translate, type);
    const legacy = getCertificateFieldTokenLegacy(type);
    patterns.push(localized);
    if (localized !== legacy) {
      patterns.push(legacy);
    }
  }
  const unique = [...new Set(patterns)].sort((a, b) => b.length - a.length);
  const inner = unique.map(p => escapeRegExp(p)).join('|');
  return new RegExp(`(${inner})`, 'g');
}

type TextPart = { value: string; token: boolean };

export function parseCertificateFieldTextParts(
  text: string,
  translate: (key: string) => string,
): TextPart[] {
  if (!text) return [{ value: '', token: false }];
  const regex = buildTokenSplitRegex(translate);
  const parts = text.split(regex);
  const tokenSet = new Set<string>();
  for (const type of certificateAdditionalFieldTypes) {
    tokenSet.add(getCertificateFieldToken(translate, type));
    tokenSet.add(getCertificateFieldTokenLegacy(type));
  }
  return parts.map(part => ({ value: part, token: tokenSet.has(part) }));
}

export const getCertificateFieldTypesInText = (
  text: string,
  translate: (key: string) => string,
): Set<CertificateAdditionalFieldType> => {
  const result = new Set<CertificateAdditionalFieldType>();
  if (!text) return result;
  for (const type of certificateAdditionalFieldTypes) {
    const localized = getCertificateFieldToken(translate, type);
    const legacy = getCertificateFieldTokenLegacy(type);
    if (text.includes(localized) || text.includes(legacy)) {
      result.add(type);
    }
  }
  return result;
};

/**
 * Returns only the literal text the user typed, dropping variable tokens. Pills
 * stand in for runtime values of unknown length, so they don't consume the
 * character budget shown in the counter or enforced by `maxLength`.
 */
export const stripCertificateFieldTokens = (
  text: string,
  t: TCustomFunction,
): string =>
  parseCertificateFieldTextParts(text, t)
    .filter(part => !part.token)
    .map(part => part.value)
    .join('');

/** Character count that excludes variable tokens (see `stripCertificateFieldTokens`). */
export const getCertificateFieldTextLength = (
  text: string,
  t: TCustomFunction,
): number => stripCertificateFieldTokens(text, t).length;

/** Resolves a token string (localized or legacy) to its field type, if known. */
export const getCertificateFieldTypeForToken = (
  token: string,
  t: TCustomFunction,
): CertificateAdditionalFieldType | null => {
  for (const type of certificateAdditionalFieldTypes) {
    if (
      token === getCertificateFieldToken(t, type) ||
      token === getCertificateFieldTokenLegacy(type)
    ) {
      return type;
    }
  }
  return null;
};

/**
 * Enforces the "one pill per variable" invariant: removes any token whose field
 * type already appeared earlier in the text, keeping the first occurrence. Used
 * to reconcile the editor after native undo/redo, which can resurrect a removed
 * pill and leave two pills for the same variable.
 */
export const dedupeCertificateFieldTokens = (
  text: string,
  t: TCustomFunction,
): string => {
  const seen = new Set<string>();
  let result = '';
  for (const part of parseCertificateFieldTextParts(text, t)) {
    if (part.token) {
      const type = getCertificateFieldTypeForToken(part.value, t);
      const key = type ?? part.value;
      if (seen.has(key)) continue;
      seen.add(key);
    }
    result += part.value;
  }
  return result;
};
