/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {
  type EmbeddedData,
  type ModelData,
  DataElement,
  RelationalCSVData,
  type Database,
  type RelationalCSVDataTable,
  DataElementReference,
  ExternalFormatData,
  ModelStoreData,
  ModelEmbeddedData,
  type RelationElement,
  RelationElementsData,
  RelationRowTestData,
  observe_RelationRowTestData,
  observe_RelationElement,
} from '@finos/legend-graph';
import {
  ContentType,
  csvDecodeValue,
  csvEncodeValue,
  csvStringify,
  parseCSVContent,
  guaranteeNonEmptyString,
  tryToFormatLosslessJSONString,
  UnsupportedOperationError,
  uuid,
} from '@finos/legend-shared';
import { action, makeObservable, observable } from 'mobx';
import type { DSL_Data_LegendStudioApplicationPlugin_Extension } from '../../../../extensions/DSL_Data_LegendStudioApplicationPlugin_Extension.js';
import type { EditorStore } from '../../../EditorStore.js';
import {
  dataElementReference_setDataElement,
  externalFormatData_setContentType,
  externalFormatData_setData,
  relationalData_addTable,
  relationalData_deleteData,
  relationalData_setTableValues,
} from '../../../../graph-modifier/DSL_Data_GraphModifierHelper.js';
import { EmbeddedDataType } from '../../ExternalFormatState.js';
import { TEMPORARY__createRelationalDataFromCSV } from '../../../utils/TestableUtils.js';

export const createEmbeddedData = (
  type: string,
  editorStore: EditorStore,
): EmbeddedData => {
  if (type === EmbeddedDataType.EXTERNAL_FORMAT_DATA) {
    const externalFormatData = new ExternalFormatData();
    externalFormatData_setData(externalFormatData, '');
    externalFormatData_setContentType(
      externalFormatData,
      guaranteeNonEmptyString(
        editorStore.graphState.graphGenerationState.externalFormatState
          .formatContentTypes[0],
      ),
    );
    return externalFormatData;
  } else if (type === EmbeddedDataType.RELATIONAL_CSV) {
    const relational = new RelationalCSVData();
    return relational;
  } else if (type === EmbeddedDataType.RELATION_ELEMENTS_DATA) {
    const testData = new RelationElementsData();
    return testData;
  } else if (type === EmbeddedDataType.MODEL_STORE_DATA) {
    const modelStoreData = new ModelStoreData();
    return modelStoreData;
  } else {
    const extraEmbeddedDataCreator = editorStore.pluginManager
      .getApplicationPlugins()
      .flatMap(
        (plugin) =>
          (
            plugin as DSL_Data_LegendStudioApplicationPlugin_Extension
          ).getExtraEmbeddedDataCreators?.() ?? [],
      );
    for (const creator of extraEmbeddedDataCreator) {
      const embeddedData = creator(type);
      if (embeddedData) {
        return embeddedData;
      }
    }
    throw new UnsupportedOperationError(
      `Can't create embedded data: no compatible creators available from plugins`,
      type,
    );
  }
};

export abstract class EmbeddedDataState {
  editorStore: EditorStore;
  embeddedData: EmbeddedData;

  constructor(editorStore: EditorStore, embeddedData: EmbeddedData) {
    this.editorStore = editorStore;
    this.embeddedData = embeddedData;
  }

  abstract label(): string;
}

export class ExternalFormatDataState extends EmbeddedDataState {
  override embeddedData: ExternalFormatData;
  canEditContentType = true;

  constructor(editorStore: EditorStore, embeddedData: ExternalFormatData) {
    super(editorStore, embeddedData);
    makeObservable(this, {
      format: action,
      canEditContentType: observable,
    });
    this.embeddedData = embeddedData;
  }

  label(): string {
    return 'External Format Data';
  }

  setCanEditoContentType(val: boolean): void {
    this.canEditContentType = val;
  }

  get supportsFormatting(): boolean {
    return this.embeddedData.contentType === ContentType.APPLICATION_JSON;
  }

  format(): void {
    externalFormatData_setData(
      this.embeddedData,
      tryToFormatLosslessJSONString(this.embeddedData.data),
    );
  }
}

export abstract class ModelDataState {
  readonly uuid = uuid();
  readonly modelStoreDataState: ModelStoreDataState;
  modelData: ModelData;

  constructor(modelData: ModelData, modelStoreDataState: ModelStoreDataState) {
    this.modelStoreDataState = modelStoreDataState;
    this.modelData = modelData;
  }
}

export class ModelEmbeddedDataState extends ModelDataState {
  override modelData: ModelEmbeddedData;
  embeddedDataState: EmbeddedDataState;

  constructor(
    modelData: ModelEmbeddedData,
    modelStoreDataState: ModelStoreDataState,
  ) {
    super(modelData, modelStoreDataState);
    this.modelData = modelData;
    this.embeddedDataState = buildEmbeddedDataEditorState(
      this.modelData.data,
      this.modelStoreDataState.editorStore,
    );
  }
}

export class UnsupportedModelDataState extends ModelDataState {}

export class ModelStoreDataState extends EmbeddedDataState {
  override embeddedData: ModelStoreData;
  modelDataStates: ModelDataState[] = [];
  hideClass = false;

  constructor(
    editorStore: EditorStore,
    embeddedData: ModelStoreData,
    hideClass?: boolean,
  ) {
    super(editorStore, embeddedData);
    makeObservable(this, {
      hideClass: observable,
      modelDataStates: observable,
      buildStates: action,
    });
    this.embeddedData = embeddedData;
    this.modelDataStates = this.buildStates();
    this.hideClass = Boolean(hideClass);
  }

  label(): string {
    return 'Model Store Data';
  }

  buildStates(): ModelDataState[] {
    return (
      this.embeddedData.modelData?.map((modelData) => {
        if (modelData instanceof ModelEmbeddedData) {
          return new ModelEmbeddedDataState(modelData, this);
        }
        return new UnsupportedModelDataState(modelData, this);
      }) ?? []
    );
  }
}

export class RelationElementState {
  relationElement: RelationElement;
  supportsColumnEditing: boolean;

  constructor(
    relationElement: RelationElement,
    options?: { supportsColumnEditing?: boolean },
  ) {
    makeObservable(this, {
      relationElement: observable,
      addColumn: action,
      removeColumn: action,
      updateColumn: action,
      addRow: action,
      removeRow: action,
      updateRow: action,
      clearAllData: action,
      importCSV: action,
    });
    this.supportsColumnEditing = options?.supportsColumnEditing ?? true;
    this.relationElement = relationElement;
    this.relationElement = observe_RelationElement(relationElement);
  }

  addColumn(name: string): void {
    this.relationElement.columns.push(name);
    this.relationElement.rows.forEach((row) => {
      row.values.push('');
    });
  }

  removeColumn(index: number): void {
    const columnToRemove = this.relationElement.columns[index];
    if (columnToRemove) {
      this.relationElement.columns.splice(index, 1);
      this.relationElement.rows.forEach((row) => {
        row.values.splice(index, 1);
      });
    }
  }

  updateColumn(index: number, name: string): void {
    const oldName = this.relationElement.columns[index];
    if (oldName && oldName !== name) {
      this.relationElement.columns[index] = name;
    }
  }

  addRow(): void {
    const row = new RelationRowTestData();
    row.values = [];
    const newRow = observe_RelationRowTestData(row);
    this.relationElement.columns.forEach((col) => {
      newRow.values.push('');
    });
    this.relationElement.rows.push(newRow);
  }

  removeRow(index: number): void {
    this.relationElement.rows.splice(index, 1);
  }

  updateRow(rowIndex: number, columnIndex: number, value: string): void {
    if (this.relationElement.rows[rowIndex]) {
      this.relationElement.rows[rowIndex].values[columnIndex] =
        csvEncodeValue(value);
    }
  }

  getDisplayValue(rowIndex: number, columnIndex: number): string {
    const value = this.relationElement.rows[rowIndex]?.values[columnIndex];
    return value !== undefined ? csvDecodeValue(value) : '';
  }

  clearAllData(): void {
    this.relationElement.rows.splice(0);
  }

  exportJSON(): string {
    return JSON.stringify(
      {
        columns: this.relationElement.columns,
        data: this.relationElement.rows.map((row) => ({
          values: row.values.map((v) => csvDecodeValue(v)),
        })),
      },
      null,
      2,
    );
  }

  exportSQL(): string {
    if (
      this.relationElement.columns.length === 0 ||
      this.relationElement.rows.length === 0
    ) {
      return '';
    }

    const tableName = 'test_data';
    const defaultDataType = 'VARCHAR(1000)';
    const columnDefs = this.relationElement.columns
      .map((col) => `${col} ${defaultDataType}`)
      .join(', ');
    const createTable = `CREATE TABLE ${tableName} (${columnDefs});`;

    const insertStatements = this.relationElement.rows.map((row) => {
      const values = this.relationElement.columns
        .map((col, colIndex) => {
          const value = csvDecodeValue(row.values[colIndex] ?? '');
          if (value !== '') {
            return `'${value.replace(/'/g, "''")}'`;
          }
          return 'NULL';
        })
        .join(', ');
      return `INSERT INTO ${tableName} VALUES (${values});`;
    });

    return [createTable, '', ...insertStatements].join('\n');
  }

  exportCSV(): string {
    // decode so that csvStringify does not double encode
    const data = this.relationElement.rows.map((row) =>
      row.values.map((v) => csvDecodeValue(v)),
    );
    return csvStringify([this.relationElement.columns, ...data]);
  }

  importCSV(csvContent: string): void {
    const parsed = parseCSVContent(csvContent);
    if (parsed.length === 0) {
      return;
    }

    const headers = parsed[0];
    if (!headers) {
      return;
    }

    this.relationElement.columns = headers;
    this.relationElement.rows = parsed.slice(1).map((values) => {
      const row = new RelationRowTestData();
      row.values = headers.map((_, index) =>
        csvEncodeValue(values[index] ?? ''),
      );
      return observe_RelationRowTestData(row);
    });
  }
}

export interface RelationElementAccessorOption {
  label: string;
  value: string;
  columns: string[];
}

export class RelationElementsDataState extends EmbeddedDataState {
  override embeddedData: RelationElementsData;
  showImportCSVModal = false;
  showNewRelationElementModal = false;
  activeRelationElement: RelationElementState | undefined;
  relationElementStates: RelationElementState[];
  accessorOptions: RelationElementAccessorOption[] | undefined;
  accessorTypeLabel: string | undefined;
  refreshAccessorOptions: (() => Promise<void>) | undefined;

  constructor(editorStore: EditorStore, embeddedData: RelationElementsData) {
    super(editorStore, embeddedData);
    makeObservable(this, {
      embeddedData: observable,
      showImportCSVModal: observable,
      showNewRelationElementModal: observable,
      activeRelationElement: observable,
      relationElementStates: observable,
      accessorOptions: observable,
      accessorTypeLabel: observable,
      setActiveRelationElement: action,
      setShowImportCSVModal: action,
      setShowNewRelationElementModal: action,
      addRelationElement: action,
      deleteRelationElement: action,
      setAccessorOptions: action,
    });
    this.embeddedData = embeddedData;
    this.relationElementStates = embeddedData.relationElements.map(
      (relationElement) => new RelationElementState(relationElement),
    );
    this.activeRelationElement = this.relationElementStates[0];
  }

  label(): string {
    return 'Relation Elements Test Data';
  }

  setActiveRelationElement(val: RelationElementState | undefined): void {
    this.activeRelationElement = val;
  }

  addRelationElement(relationElement: RelationElement): void {
    const newElementState = new RelationElementState(relationElement);
    this.relationElementStates.push(newElementState);
    this.embeddedData.relationElements.push(relationElement);
    this.setActiveRelationElement(newElementState);
  }

  deleteRelationElement(relationElementState: RelationElementState): void {
    const idx = this.relationElementStates.indexOf(relationElementState);
    if (idx === -1) {
      return;
    }
    this.relationElementStates.splice(idx, 1);
    this.embeddedData.relationElements.splice(idx, 1);
    if (this.activeRelationElement === relationElementState) {
      this.setActiveRelationElement(this.relationElementStates[0]);
    }
  }

  setShowImportCSVModal(show: boolean): void {
    this.showImportCSVModal = show;
  }

  setShowNewRelationElementModal(show: boolean): void {
    this.showNewRelationElementModal = show;
  }

  setAccessorOptions(
    options: RelationElementAccessorOption[] | undefined,
    typeLabel: string | undefined,
  ): void {
    this.accessorOptions = options;
    this.accessorTypeLabel = typeLabel;
  }

  setRefreshAccessorOptions(fn: (() => Promise<void>) | undefined): void {
    this.refreshAccessorOptions = fn;
  }

  get availableAccessorOptions(): RelationElementAccessorOption[] {
    if (!this.accessorOptions) {
      return [];
    }
    const existingPaths = new Set(
      this.relationElementStates.map((s) => s.relationElement.paths.join('.')),
    );
    return this.accessorOptions.filter((opt) => !existingPaths.has(opt.value));
  }
}

export class RelationalCSVDataTableState {
  readonly editorStore: EditorStore;
  table: RelationalCSVDataTable;
  constructor(table: RelationalCSVDataTable, editorStore: EditorStore) {
    this.table = table;
    this.editorStore = editorStore;

    makeObservable(this, {
      table: observable,
      updateTableValues: action,
    });
  }

  updateTableValues(val: string): void {
    relationalData_setTableValues(this.table, val);
  }
}

export class RelationalCSVDataState extends EmbeddedDataState {
  override embeddedData: RelationalCSVData;
  selectedTable: RelationalCSVDataTableState | undefined;
  showImportCSVModal = false;
  database: Database | undefined;

  //
  showTableIdentifierModal = false;
  tableToEdit: RelationalCSVDataTable | undefined;

  constructor(editorStore: EditorStore, embeddedData: RelationalCSVData) {
    super(editorStore, embeddedData);
    makeObservable(this, {
      selectedTable: observable,
      showTableIdentifierModal: observable,
      deleteTable: observable,
      showImportCSVModal: observable,
      database: observable,
      resetSelectedTable: action,
      changeSelectedTable: action,
      setDatabase: action,
      closeModal: action,
      openIdentifierModal: action,
      setShowImportCsvModal: action,
      closeCSVModal: action,
      importCSV: action,
    });
    this.embeddedData = embeddedData;
    this.resetSelectedTable();
  }

  setShowImportCsvModal(val: boolean): void {
    this.showImportCSVModal = val;
  }

  setDatabase(val: Database | undefined): void {
    this.database = val;
  }

  openIdentifierModal(renameTable?: RelationalCSVDataTable | undefined): void {
    this.showTableIdentifierModal = true;
    this.tableToEdit = renameTable;
  }

  closeCSVModal(): void {
    this.showImportCSVModal = false;
  }

  closeModal(): void {
    this.showTableIdentifierModal = false;
    this.tableToEdit = undefined;
  }

  importCSV(val: string): void {
    const generated = TEMPORARY__createRelationalDataFromCSV(val);
    generated.tables.forEach((t) =>
      relationalData_addTable(this.embeddedData, t),
    );
    this.resetSelectedTable();
  }

  resetSelectedTable(): void {
    const table = this.embeddedData.tables[0];
    if (table) {
      this.selectedTable = new RelationalCSVDataTableState(
        table,
        this.editorStore,
      );
    } else {
      this.selectedTable = undefined;
    }
  }

  deleteTable(val: RelationalCSVDataTable): void {
    relationalData_deleteData(this.embeddedData, val);
    if (this.selectedTable?.table === val) {
      this.resetSelectedTable();
    }
  }

  changeSelectedTable(val: RelationalCSVDataTable): void {
    this.selectedTable = new RelationalCSVDataTableState(val, this.editorStore);
  }

  label(): string {
    return 'Relational Data';
  }
}
export interface EmbeddedDataStateOption {
  hideSource?: boolean;
}
export class UnsupportedDataState extends EmbeddedDataState {
  label(): string {
    return 'Unsupported embedded data';
  }
}

export class DataElementReferenceState extends EmbeddedDataState {
  override embeddedData: DataElementReference;
  embeddedDataValueState: EmbeddedDataState;
  options?: EmbeddedDataStateOption | undefined;

  constructor(
    editorStore: EditorStore,
    embeddedData: DataElementReference,
    options?: EmbeddedDataStateOption,
  ) {
    super(editorStore, embeddedData);
    this.embeddedData = embeddedData;
    this.options = options;
    this.embeddedDataValueState = this.buildValueState();
  }

  label(): string {
    return 'Data Element Reference';
  }

  setDataElement(dataElement: DataElement): void {
    dataElementReference_setDataElement(
      this.embeddedData,
      dataElement,
      this.editorStore.changeDetectionState.observerContext,
    );
    this.embeddedDataValueState = this.buildValueState();
  }

  buildValueState(options?: EmbeddedDataStateOption): EmbeddedDataState {
    const packagableEl = this.embeddedData.dataElement.value;
    if (packagableEl instanceof DataElement) {
      return buildEmbeddedDataEditorState(
        packagableEl.data,
        this.editorStore,
        this.options,
      );
    }
    return new UnsupportedDataState(this.editorStore, this.embeddedData);
  }
}

export function buildEmbeddedDataEditorState(
  _embeddedData: EmbeddedData,
  editorStore: EditorStore,
  options?: EmbeddedDataStateOption,
): EmbeddedDataState {
  const embeddedData = _embeddedData;
  if (embeddedData instanceof ExternalFormatData) {
    return new ExternalFormatDataState(editorStore, embeddedData);
  } else if (embeddedData instanceof ModelStoreData) {
    return new ModelStoreDataState(
      editorStore,
      embeddedData,
      options?.hideSource,
    );
  } else if (embeddedData instanceof RelationalCSVData) {
    return new RelationalCSVDataState(editorStore, embeddedData);
  } else if (embeddedData instanceof RelationElementsData) {
    return new RelationElementsDataState(editorStore, embeddedData);
  } else if (embeddedData instanceof DataElementReference) {
    return new DataElementReferenceState(editorStore, embeddedData, options);
  } else {
    const extraEmbeddedDataEditorStateBuilders = editorStore.pluginManager
      .getApplicationPlugins()
      .flatMap(
        (plugin) =>
          (
            plugin as DSL_Data_LegendStudioApplicationPlugin_Extension
          ).getExtraEmbeddedDataEditorStateBuilders?.() ?? [],
      );
    for (const stateBuilder of extraEmbeddedDataEditorStateBuilders) {
      const state = stateBuilder(editorStore, embeddedData);
      if (state) {
        return state;
      }
    }
    return new UnsupportedDataState(editorStore, embeddedData);
  }
}
