// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { parse as parseIcuMessage } from '@formatjs/icu-messageformat-parser';
import type {
  MessageFormatElement,
  Location,
} from '@formatjs/icu-messageformat-parser';
import parseJsonToAst from 'json-to-ast';
import { readFile } from 'node:fs/promises';
import { join as pathJoin, relative as pathRelative } from 'node:path';
import { styleText } from 'node:util';
import { deepEqual } from 'node:assert';
import type { Rule } from './utils/rule.std.ts';

import icuPrefix from './rules/icuPrefix.std.ts';
import wrapEmoji from './rules/wrapEmoji.std.ts';
import onePlural from './rules/onePlural.std.ts';
import noLegacyVariables from './rules/noLegacyVariables.std.ts';
import noNestedChoice from './rules/noNestedChoice.std.ts';
import noOffset from './rules/noOffset.std.ts';
import noOneChoice from './rules/noOneChoice.std.ts';
import noOrdinal from './rules/noOrdinal.std.ts';
import pluralPound from './rules/pluralPound.std.ts';

const RULES = [
  icuPrefix,
  wrapEmoji,
  noLegacyVariables,
  noNestedChoice,
  noOffset,
  noOneChoice,
  noOrdinal,
  onePlural,
  pluralPound,
];

type Test = {
  messageformat: string;
  expectErrors: Array<string>;
};

const tests: Record<string, Test> = {
  'icu:err1': {
    messageformat:
      '{a, plural, one {a} other {as}} {b, plural, one {b} other {bs}}',
    expectErrors: ['onePlural'],
  },
  'icu:err2': {
    messageformat:
      '{a, plural, one {a} other {{b, plural, one {b} other {bs}}}}',
    expectErrors: ['noNestedChoice', 'onePlural'],
  },
  'icu:err3': {
    messageformat:
      '{a, select, one {a} other {{b, select, one {b} other {bs}}}}',
    expectErrors: ['noNestedChoice'],
  },
  'icu:err4': {
    messageformat: '{a, plural, offset:1 one {a} other {as}}',
    expectErrors: ['noOffset'],
  },
  'icu:noOneChoice:1': {
    messageformat: '{a, plural, other {a}}',
    expectErrors: ['noOneChoice'],
  },
  'icu:err5': {
    messageformat: '{a, selectordinal, one {a} other {as}}',
    expectErrors: ['noOrdinal'],
  },
  'icu:err6': {
    messageformat: '$a$',
    expectErrors: ['noLegacyVariables'],
  },
  'icu:wrapEmoji:1': {
    messageformat: '👩',
    expectErrors: ['wrapEmoji'],
  },
  'icu:wrapEmoji:2': {
    messageformat: '<emojify>👩 extra</emojify>',
    expectErrors: ['wrapEmoji'],
  },
  'icu:wrapEmoji:3': {
    messageformat: '<emojify>👩👩</emojify>',
    expectErrors: ['wrapEmoji'],
  },
  'icu:wrapEmoji:4': {
    messageformat: '<emojify>{emoji}</emojify>',
    expectErrors: ['wrapEmoji'],
  },
  'icu:wrapEmoji:5': {
    messageformat: '<emojify>👩</emojify>',
    expectErrors: [],
  },
};

type Report = {
  id: string;
  message: string;
  location: Location | void;
  locationOffset: number;
};

function lintMessage(
  messageId: string,
  elements: Array<MessageFormatElement>,
  rules: Array<Rule>
) {
  const reports: Array<Report> = [];
  for (const rule of rules) {
    rule.run(elements, {
      messageId,
      report(message, location, locationOffset = 0) {
        reports.push({ id: rule.id, message, location, locationOffset });
      },
    });
  }
  return reports;
}

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

async function lintMessages() {
  const repoRoot = pathJoin(__dirname, '..', '..');
  const filePath = pathJoin(repoRoot, '_locales/en/messages.json');
  const relativePath = pathRelative(repoRoot, filePath);
  let file = await readFile(filePath, 'utf-8');

  if (process.argv.includes('--test')) {
    file = JSON.stringify(tests);
  }

  const jsonAst = parseJsonToAst(file);

  let failed = false;

  assert(jsonAst.type === 'Object', 'Expected an object');
  for (const topProp of jsonAst.children) {
    if (topProp.key.value === 'smartling') {
      continue;
    }

    const messageId = topProp.key.value;

    assert(topProp.value.type === 'Object', 'Expected an object');

    const icuMessageProp = topProp.value.children.find(messageProp => {
      return messageProp.key.value === 'messageformat';
    });
    if (icuMessageProp == null) {
      continue;
    }

    const icuMesssageLiteral = icuMessageProp.value;
    assert(
      icuMesssageLiteral.type === 'Literal' &&
        typeof icuMesssageLiteral.value === 'string',
      'Expected a string'
    );

    const icuMessage: string = icuMesssageLiteral.value;

    const ast = parseIcuMessage(icuMessage, {
      captureLocation: true,
      shouldParseSkeletons: true,
      requiresOtherClause: true,
    });

    const reports = lintMessage(messageId, ast, RULES);
    const key = topProp.key.value;

    if (process.argv.includes('--test')) {
      // oxlint-disable-next-line typescript/no-non-null-assertion
      const test = tests[key]!;
      const actualErrors = reports.map(report => report.id);
      deepEqual(actualErrors, test.expectErrors);
      continue;
    }

    for (const report of reports) {
      let loc = '';

      if (report.location != null && icuMesssageLiteral.loc != null) {
        const line =
          icuMesssageLiteral.loc.start.line + (report.location.start.line - 1);
        const column =
          icuMesssageLiteral.loc.start.column +
          report.location.start.column +
          report.locationOffset;
        loc = `:${line}:${column}`;
      } else if (icuMesssageLiteral.loc != null) {
        const { line, column } = icuMesssageLiteral.loc.start;
        loc = `:${line}:${column + report.locationOffset}`;
      }

      // oxlint-disable-next-line no-console
      console.error(
        styleText(['bold', 'cyan'], `${relativePath}${loc}`) +
          `${report.message} ` +
          styleText('magenta', `(${styleText('underline', report.id)})`)
      );
      // oxlint-disable-next-line no-console
      console.error(
        styleText('dim', `in ${key} is "`) +
          styleText('red', icuMessage) +
          styleText('dim', '"')
      );
      // oxlint-disable-next-line no-console
      console.error();

      failed = true;
    }
  }

  if (failed) {
    process.exit(1);
  }
}

// oxlint-disable-next-line promise/prefer-await-to-then
lintMessages().catch(error => {
  // oxlint-disable-next-line no-console
  console.error(error);
  process.exit(1);
});
