import { DataCapsule, TaskState } from '@wix/bex-core';
import {
  action,
  computed,
  makeObservable,
  observable,
  runInAction,
} from 'mobx';
import { indexOfCompareFn } from '../indexOfCompareFn';

const STORAGE_KEY = 'schema-card-layout';

export interface SchemaViewConfig {
  id: string;
  isVisible?: boolean;
  hideable?: boolean; // not persisted — consumer-only; false = always visible
}

type StoredLayout = {
  fields: Pick<SchemaViewConfig, 'id' | 'isVisible'>[];
};

export interface SchemaFieldsStateParams {
  dataCapsule: DataCapsule;
  fields: SchemaViewConfig[];
}

export class SchemaFieldsState {
  _storedFields: SchemaViewConfig[] = [];
  readonly initTask = new TaskState();
  private _persistQueue: Promise<void> = Promise.resolve();
  private readonly params: SchemaFieldsStateParams;

  constructor(params: SchemaFieldsStateParams) {
    this.params = params;
    makeObservable(this, {
      _storedFields: observable,
      orderedFields: computed,
      setFieldVisibility: action.bound,
      setFieldOrder: action.bound,
    });
  }

  init() {
    this.initTask.runOnce(async () => {
      try {
        await this._fetchFromStorage();
      } catch (e) {
        console.error(e);
      }
    });
  }

  get orderedFields(): SchemaViewConfig[] {
    if (!this._storedFields.length) {
      return this.params.fields;
    }
    // Fields not in storage all get index -1; stable sort preserves their declaration order
    return [...this.params.fields].sort(
      indexOfCompareFn(this._storedFields, (f) => f?.id),
    );
  }

  isFieldVisible(id: string): boolean {
    const field = this.params.fields.find((f) => f.id === id);
    if (!field) {
      return true;
    } // unknown id — render by default
    if (field.hideable === false) {
      return true;
    } // non-hideable — always visible
    const stored = this._storedFields.find((f) => f.id === id);
    return stored ? stored.isVisible ?? true : field.isVisible ?? true;
  }

  setFieldVisibility(id: string, isVisible: boolean) {
    const idx = this._storedFields.findIndex((f) => f.id === id);
    if (idx >= 0) {
      // Replace element (not mutate in-place) so MobX tracks the array change
      this._storedFields[idx] = { ...this._storedFields[idx], isVisible };
    } else {
      this._storedFields.push({ id, isVisible });
    }
    // Wait for init to complete before persisting — prevents overwriting the
    // full stored layout when called before _fetchFromStorage resolves.
    // Serialize writes: chain onto the queue so rapid toggles don't race.
    this._persistQueue = this._persistQueue
      .then(() => this.initTask.status.promise)
      .then(() => this.persistFields())
      .catch((e) => console.error(e));
  }

  setFieldOrder(orderedFields: SchemaViewConfig[]) {
    const storedMap = new Map(this._storedFields.map((f) => [f.id, f]));
    const declaredMap = new Map(this.params.fields.map((f) => [f.id, f]));

    // Rebuild _storedFields in the new order, preserving existing visibility.
    // Only include fields that are still declared; unknown ids are silently dropped.
    this._storedFields = orderedFields
      .filter((f) => declaredMap.has(f.id))
      .map(
        (f) =>
          storedMap.get(f.id) ?? {
            id: f.id,
            isVisible: declaredMap.get(f.id)!.isVisible ?? true,
          },
      );

    this._persistQueue = this._persistQueue
      .then(() => this.initTask.status.promise)
      .then(() => this.persistFields())
      .catch((e) => console.error(e));
  }

  private async persistFields() {
    const fields = this._storedFields.map(({ id, isVisible }) => ({
      id,
      isVisible,
    }));
    await this.params.dataCapsule.setItem(STORAGE_KEY, { fields });
  }

  async _fetchFromStorage() {
    const { dataCapsule, fields } = this.params;
    let storedLayout: StoredLayout | null = null;
    try {
      const raw = await dataCapsule.getItem<StoredLayout>(STORAGE_KEY);
      storedLayout =
        raw && typeof raw === 'object' && Array.isArray(raw.fields)
          ? raw
          : null;
    } catch (e) {
      if (e instanceof Error && e.message === 'Key was not found in capsule') {
        storedLayout = null;
      } else {
        throw e;
      }
    }
    if (!storedLayout) {
      return;
    }

    // Merge: keep stored order/visibility; append new fields; drop removed ones.
    // Dedup by id — keeps the first occurrence, discarding any corrupt duplicates.
    const declaredIds = new Set(fields.map((f) => f.id));
    const seenIds = new Set<string>();
    const merged: SchemaViewConfig[] = storedLayout.fields.filter((f) => {
      if (!f?.id || !declaredIds.has(f.id) || seenIds.has(f.id)) {
        return false;
      }
      seenIds.add(f.id);
      return true;
    });
    fields.forEach((f) => {
      if (!merged.some((s) => s.id === f.id)) {
        merged.push({ id: f.id, isVisible: f.isVisible ?? true });
      }
    });
    runInAction(() => {
      this._storedFields = merged;
    });
  }
}
