import {
  CustomFields,
  Rundown,
  OntimeEvent,
  OntimeBlock,
  EntryCustomFields,
  SupportedEntry,
  isOntimeBlock,
  TimerType,
  CustomFieldKey,
} from 'ontime-types';
import {
  ImportMap,
  defaultImportMap,
  generateId,
  isKnownTimerType,
  validateTimerType,
  validateEndAction,
  customFieldLabelToKey,
  isAlphanumericWithSpace,
} from 'ontime-utils';

import { Merge } from 'ts-essentials';

import { is } from '../../utils/is.js';
import { makeString } from '../../utils/parserUtils.js';
import { parseExcelDate } from '../../utils/time.js';

/**
 * @description Excel array parser
 * @param {array} excelData - array with excel sheet
 * @param {ImportOptions} options - an object that contains the import map
 * @returns {object} - parsed object
 */
export const parseExcel = (
  excelData: unknown[][],
  existingCustomFields: CustomFields,
  sheetName: string = 'Rundown from excel',
  options?: Partial<ImportMap>,
): {
  rundown: Rundown;
  customFields: CustomFields;
  rundownMetadata: Record<string, { row: number; col: number }>;
} => {
  const rundownMetadata: Record<string, { row: number; col: number }> = {};
  const importMap: ImportMap = { ...defaultImportMap, ...options };

  for (const [key, value] of Object.entries(importMap)) {
    if (is.string(value)) {
      // @ts-expect-error -- we are sure that the key exists
      importMap[key] = value.toLowerCase().trim();
    }
  }

  const { mergedCustomFields, customFieldImportKeys } = getCustomFieldData(importMap, existingCustomFields);
  const rundown: Rundown = {
    id: generateId(),
    title: sheetName,
    order: [],
    flatOrder: [],
    entries: {},
    revision: 0,
  };

  // title stuff: strings
  let titleIndex: number | null = null;
  let cueIndex: number | null = null;
  let notesIndex: number | null = null;
  let colourIndex: number | null = null;

  // options: booleans
  let skipIndex: number | null = null;
  let countToEndIndex: number | null = null;

  let linkStartIndex: number | null = null;

  // times: numbers
  let timeStartIndex: number | null = null;
  let timeEndIndex: number | null = null;
  let durationIndex: number | null = null;
  let timeWarningIndex: number | null = null;
  let timeDangerIndex: number | null = null;

  // options: enum properties
  let endActionIndex: number | null = null;
  let timerTypeIndex: number | null = null;

  //ID
  let entryIdIndex: number | null = null;

  // record of column index and the name of the field
  const customFieldIndexes: Record<number, string> = {};

  excelData.forEach((row, rowIndex) => {
    if (row.length === 0) {
      return;
    }

    // TODO: extract generating handlers from importMap
    const handlers = {
      [importMap.timeStart]: (row: number, col: number) => {
        timeStartIndex = col;
        rundownMetadata['timeStart'] = { row, col };
      },
      [importMap.linkStart]: (row: number, col: number) => {
        linkStartIndex = col;
        rundownMetadata['linkStart'] = { row, col };
      },
      [importMap.timeEnd]: (row: number, col: number) => {
        timeEndIndex = col;
        rundownMetadata['timeEnd'] = { row, col };
      },
      [importMap.duration]: (row: number, col: number) => {
        durationIndex = col;
        rundownMetadata['duration'] = { row, col };
      },

      [importMap.cue]: (row: number, col: number) => {
        cueIndex = col;
        rundownMetadata['cue'] = { row, col };
      },
      [importMap.title]: (row: number, col: number) => {
        titleIndex = col;
        rundownMetadata['title'] = { row, col };
      },
      [importMap.countToEnd]: (row: number, col: number) => {
        countToEndIndex = col;
        rundownMetadata['countToEnd'] = { row, col };
      },
      [importMap.skip]: (row: number, col: number) => {
        skipIndex = col;
        rundownMetadata['skip'] = { row, col };
      },
      [importMap.note]: (row: number, col: number) => {
        notesIndex = col;
        rundownMetadata['note'] = { row, col };
      },
      [importMap.colour]: (row: number, col: number) => {
        colourIndex = col;
        rundownMetadata['colour'] = { row, col };
      },
      [importMap.endAction]: (row: number, col: number) => {
        endActionIndex = col;
        rundownMetadata['endAction'] = { row, col };
      },
      [importMap.timerType]: (row: number, col: number) => {
        timerTypeIndex = col;
        rundownMetadata['timerType'] = { row, col };
      },
      [importMap.timeWarning]: (row: number, col: number) => {
        timeWarningIndex = col;
        rundownMetadata['timeWarning'] = { row, col };
      },
      [importMap.timeDanger]: (row: number, col: number) => {
        timeDangerIndex = col;
        rundownMetadata['timeDanger'] = { row, col };
      },
      [importMap.entryId]: (row: number, col: number) => {
        entryIdIndex = col;
        rundownMetadata['id'] = { row, col };
      },
      custom: (row: number, col: number, columnText: string, ontimeKey: string) => {
        customFieldIndexes[col] = columnText;
        rundownMetadata[`custom:${ontimeKey}`] = { row, col };
      },
    } as const;

    const entry: Partial<Merge<OntimeEvent, OntimeBlock>> = {};
    const entryCustomFields: EntryCustomFields = {};

    for (let j = 0; j < row.length; j++) {
      const column = row[j];
      // 1. we check if we have set a flag for a known field
      if (j === timerTypeIndex) {
        const maybeTimeType = makeString(column, '');
        if (maybeTimeType === 'block') {
          // we leave this as a clue for the object filtering later on
          entry.type = SupportedEntry.Block;
        } else if (maybeTimeType === '' || maybeTimeType === 'event' || isKnownTimerType(maybeTimeType)) {
          // @ts-expect-error -- we leave this as a clue for the object filtering later on
          entry.type = SupportedEntry.Event;
          entry.timerType = validateTimerType(maybeTimeType);
        } else {
          // if it is not a block or a known type, we dont import it
          return;
        }
      } else if (j === titleIndex) {
        entry.title = makeString(column, '');
      } else if (j === timeStartIndex) {
        entry.timeStart = parseExcelDate(column);
      } else if (j === linkStartIndex) {
        entry.linkStart = parseBooleanString(column);
      } else if (j === timeEndIndex) {
        entry.timeEnd = parseExcelDate(column);
      } else if (j === durationIndex) {
        entry.duration = parseExcelDate(column);
      } else if (j === cueIndex) {
        entry.cue = makeString(column, '');
      } else if (j === countToEndIndex) {
        entry.countToEnd = parseBooleanString(column);
      } else if (j === skipIndex) {
        entry.skip = parseBooleanString(column);
      } else if (j === notesIndex) {
        entry.note = makeString(column, '');
      } else if (j === endActionIndex) {
        entry.endAction = validateEndAction(column);
      } else if (j === timeWarningIndex) {
        entry.timeWarning = parseExcelDate(column);
      } else if (j === timeDangerIndex) {
        entry.timeDanger = parseExcelDate(column);
      } else if (j === colourIndex) {
        entry.colour = makeString(column, '');
      } else if (j === entryIdIndex) {
        entry.id = encodeURIComponent(makeString(column, undefined));
      } else if (j in customFieldIndexes) {
        const importKey = customFieldIndexes[j];
        const ontimeKey = customFieldImportKeys[importKey];
        entryCustomFields[ontimeKey] = makeString(column, '');
      } else {
        // 2. if there is no flag, lets see if we know the field type
        if (typeof column === 'string') {
          // we cant deal with empty content
          if (column.length === 0) {
            continue;
          }
          const columnText = column.toLowerCase().trim();

          // check if it is an ontime column
          if (handlers[columnText]) {
            // @ts-expect-error -- its ok
            handlers[columnText](rowIndex, j, undefined, undefined);
          }

          // check if it is a custom field
          if (columnText in customFieldImportKeys) {
            const ontimeKey = customFieldImportKeys[columnText];
            handlers.custom(rowIndex, j, columnText, ontimeKey);
          }

          // else. we don't know how to handle this column
          // just ignore it
        }
      }
    }

    // if we didnt find any keys (empty row, or some other data), skip making an event
    const keysFound = Object.keys(entry).length + Object.keys(entryCustomFields).length;
    if (keysFound === 0) {
      return;
    }

    const id = entry.id || generateId();
    // from excel, we can only get blocks and events
    if (isOntimeBlock(entry)) {
      const block: OntimeBlock = { ...entry, custom: { ...entryCustomFields } };
      rundown.order.push(id);
      rundown.entries[id] = block;
      return;
    }

    const event = {
      ...entry,
      custom: { ...entryCustomFields },
      type: SupportedEntry.Event,
    } as OntimeEvent;

    if (timerTypeIndex === null) {
      event.timerType = TimerType.CountDown;
    }
    rundown.order.push(id);
    rundown.flatOrder.push(id);
    rundown.entries[id] = event;
  });

  return {
    rundown,
    customFields: mergedCustomFields,
    rundownMetadata,
  };
};

/**
 * Utility function infers a boolean from a string value
 */
function parseBooleanString(value: unknown): boolean {
  if (typeof value === 'boolean') {
    return value;
  }

  // falsy values would be nullish or empty string
  if (!value || typeof value !== 'string') {
    return false;
  }
  return value.toLowerCase() !== 'false';
}

/**
 * Receives an import map which contains custom field labels and a custom fields object
 * the result importkeys is an inverted record of <importKey, ontimeKey>
 * We need this function since, when importing from sheets, the user gives us custom field labels, not keys
 * @returns the new custom fields, and a map of excel column names to ontime keys
 * @private exported for testing
 */
export function getCustomFieldData(
  importMap: ImportMap,
  existingCustomFields: CustomFields,
): {
  mergedCustomFields: CustomFields;
  customFieldImportKeys: Record<keyof CustomFields, string>;
} {
  const mergedCustomFields: CustomFields = {};
  /**
   * A map of import keys to ontime keys
   * Map<excel column name, ontime key>
   */
  const customFieldImportKeys: Record<string, CustomFieldKey> = {};

  for (const ontimeLabel in importMap.custom) {
    // if the label is not valid, we skip the import
    if (!isAlphanumericWithSpace(ontimeLabel)) {
      continue;
    }

    // generate a key for the custom field
    const keyInCustomFields = customFieldLabelToKey(ontimeLabel);
    // we lower case the excel key to make it easier to match
    const columnNameInExcel = importMap.custom[ontimeLabel].toLowerCase();
    const maybeExistingColour = existingCustomFields[keyInCustomFields]?.colour ?? '';

    // 1. add the custom field to the merged custom fields
    mergedCustomFields[keyInCustomFields] = {
      type: 'string', // we currently only support string custom fields
      colour: maybeExistingColour,
      label: ontimeLabel,
    };

    // 2. add the column to the import keys
    customFieldImportKeys[columnNameInExcel] = keyInCustomFields;
  }
  return { mergedCustomFields, customFieldImportKeys };
}
