const DEFAULT_HISTORY_LIMIT = 80;

/**
 * @typedef {Object} SchemetaPlacement
 * @property {number | null} x
 * @property {number | null} y
 * @property {number} [rotation]
 * @property {boolean} [locked]
 */

/**
 * @typedef {Object} SchemetaInstance
 * @property {string} ref
 * @property {string} [symbol]
 * @property {string} [part]
 * @property {Record<string, unknown>} [properties]
 * @property {SchemetaPlacement} placement
 */

/**
 * @typedef {Object} SchemetaModel
 * @property {Record<string, unknown>} [meta]
 * @property {Record<string, unknown>} [symbols]
 * @property {SchemetaInstance[]} instances
 * @property {Array<Record<string, unknown>>} [nets]
 * @property {Record<string, unknown>} [constraints]
 * @property {Array<Record<string, unknown>>} [annotations]
 */

/**
 * @typedef {Object} CompileResult
 * @property {string} [api_version]
 * @property {string} [schema_version]
 * @property {string} [request_id]
 * @property {boolean} ok
 * @property {Array<Record<string, unknown>>} errors
 * @property {Array<Record<string, unknown>>} warnings
 * @property {Record<string, unknown>} [layout]
 * @property {Record<string, unknown>} [layout_metrics]
 * @property {string} [render_mode_used]
 * @property {string} [svg]
 */

/**
 * @typedef {{ ref: string, pin: string } | null} SelectedPin
 */

/**
 * @typedef {Object} SelectionSlice
 * @property {string[]} selectedRefs
 * @property {string | null} selectedNet
 * @property {SelectedPin} selectedPin
 */

/**
 * @typedef {Object} ViewportSlice
 * @property {number} scale
 * @property {number} panX
 * @property {number} panY
 */

/**
 * @typedef {Object} UiFlagsSlice
 * @property {boolean} showLabels
 * @property {boolean} isolateNet
 * @property {boolean} isolateComponent
 * @property {string} renderMode
 * @property {string | null} jsonError
 */

/**
 * @typedef {Object} LifecycleSlice
 * @property {boolean} isCompiling
 * @property {string | null} lastError
 * @property {string | null} lastAction
 */

/**
 * @typedef {Object} StateSnapshot
 * @property {SchemetaModel | null} model
 * @property {CompileResult | null} compileResult
 * @property {SelectionSlice} selection
 * @property {ViewportSlice} viewport
 * @property {UiFlagsSlice} uiFlags
 */

/**
 * @typedef {Object} HistoryEntry
 * @property {string} label
 * @property {StateSnapshot} snapshot
 */

/**
 * @typedef {Object} StoreState
 * @property {SchemetaModel | null} model
 * @property {CompileResult | null} compileResult
 * @property {SelectionSlice} selection
 * @property {ViewportSlice} viewport
 * @property {UiFlagsSlice} uiFlags
 * @property {LifecycleSlice} lifecycle
 * @property {{ past: HistoryEntry[], future: HistoryEntry[], limit: number }} history
 * @property {{ active: boolean, label: string | null, before: StateSnapshot | null, dirty: boolean }} transaction
 */

/**
 * @param {unknown} value
 * @returns {any}
 */
function deepClone(value) {
  if (typeof structuredClone === "function") {
    return structuredClone(value);
  }
  return JSON.parse(JSON.stringify(value));
}

/**
 * @param {SelectionSlice | Partial<SelectionSlice>} selection
 * @returns {SelectionSlice}
 */
function normalizeSelection(selection) {
  const selectedRefs = [...new Set((selection.selectedRefs ?? []).filter(Boolean))].sort((a, b) => a.localeCompare(b));

  const selectedNet = typeof selection.selectedNet === "string" && selection.selectedNet.length > 0 ? selection.selectedNet : null;
  const selectedPin =
    selection.selectedPin && typeof selection.selectedPin.ref === "string" && typeof selection.selectedPin.pin === "string"
      ? { ref: selection.selectedPin.ref, pin: selection.selectedPin.pin }
      : null;

  return {
    selectedRefs,
    selectedNet,
    selectedPin
  };
}

/**
 * @param {Partial<ViewportSlice>} viewport
 * @param {ViewportSlice} prev
 * @returns {ViewportSlice}
 */
function normalizeViewport(viewport, prev) {
  const nextScale = Number.isFinite(viewport.scale) ? Number(viewport.scale) : prev.scale;
  const nextPanX = Number.isFinite(viewport.panX) ? Number(viewport.panX) : prev.panX;
  const nextPanY = Number.isFinite(viewport.panY) ? Number(viewport.panY) : prev.panY;

  return {
    scale: nextScale,
    panX: nextPanX,
    panY: nextPanY
  };
}

/**
 * @param {Partial<UiFlagsSlice>} uiFlags
 * @param {UiFlagsSlice} prev
 * @returns {UiFlagsSlice}
 */
function normalizeUiFlags(uiFlags, prev) {
  return {
    showLabels: typeof uiFlags.showLabels === "boolean" ? uiFlags.showLabels : prev.showLabels,
    isolateNet: typeof uiFlags.isolateNet === "boolean" ? uiFlags.isolateNet : prev.isolateNet,
    isolateComponent: typeof uiFlags.isolateComponent === "boolean" ? uiFlags.isolateComponent : prev.isolateComponent,
    renderMode: typeof uiFlags.renderMode === "string" && uiFlags.renderMode.length > 0 ? uiFlags.renderMode : prev.renderMode,
    jsonError:
      uiFlags.jsonError === null || (typeof uiFlags.jsonError === "string" && uiFlags.jsonError.length > 0)
        ? uiFlags.jsonError
        : prev.jsonError
  };
}

/**
 * @param {Partial<LifecycleSlice>} lifecycle
 * @param {LifecycleSlice} prev
 * @returns {LifecycleSlice}
 */
function normalizeLifecycle(lifecycle, prev) {
  return {
    isCompiling: typeof lifecycle.isCompiling === "boolean" ? lifecycle.isCompiling : prev.isCompiling,
    lastError:
      lifecycle.lastError === null || (typeof lifecycle.lastError === "string" && lifecycle.lastError.length > 0)
        ? lifecycle.lastError
        : prev.lastError,
    lastAction:
      lifecycle.lastAction === null || (typeof lifecycle.lastAction === "string" && lifecycle.lastAction.length > 0)
        ? lifecycle.lastAction
        : prev.lastAction
  };
}

/**
 * @param {StoreState} state
 * @returns {StateSnapshot}
 */
function snapshotState(state) {
  return {
    model: deepClone(state.model),
    compileResult: deepClone(state.compileResult),
    selection: deepClone(state.selection),
    viewport: deepClone(state.viewport),
    uiFlags: deepClone(state.uiFlags)
  };
}

/**
 * @param {StoreState} state
 * @param {StateSnapshot} snapshot
 * @returns {StoreState}
 */
function restoreSnapshot(state, snapshot) {
  return {
    ...state,
    model: deepClone(snapshot.model),
    compileResult: deepClone(snapshot.compileResult),
    selection: deepClone(snapshot.selection),
    viewport: deepClone(snapshot.viewport),
    uiFlags: deepClone(snapshot.uiFlags)
  };
}

/**
 * @param {StoreState} state
 * @param {string} label
 * @returns {StoreState}
 */
function pushHistoryEntry(state, label) {
  const nextPast = [...state.history.past, { label, snapshot: snapshotState(state) }];
  const overflow = Math.max(0, nextPast.length - state.history.limit);
  const past = overflow > 0 ? nextPast.slice(overflow) : nextPast;

  return {
    ...state,
    history: {
      ...state.history,
      past,
      future: []
    }
  };
}

/**
 * @param {StoreState} current
 * @param {StoreState} candidate
 * @returns {boolean}
 */
function hasCoreChange(current, candidate) {
  return (
    current.model !== candidate.model ||
    current.compileResult !== candidate.compileResult ||
    current.selection !== candidate.selection ||
    current.viewport !== candidate.viewport ||
    current.uiFlags !== candidate.uiFlags
  );
}

/**
 * @param {StoreState} state
 * @returns {StoreState}
 */
function undoState(state) {
  if (state.transaction.active || state.history.past.length === 0) {
    return state;
  }

  const prev = state.history.past[state.history.past.length - 1];
  const currentSnapshot = snapshotState(state);
  const restored = restoreSnapshot(state, prev.snapshot);

  return {
    ...restored,
    history: {
      ...state.history,
      past: state.history.past.slice(0, -1),
      future: [...state.history.future, { label: prev.label, snapshot: currentSnapshot }]
    }
  };
}

/**
 * @param {StoreState} state
 * @returns {StoreState}
 */
function redoState(state) {
  if (state.transaction.active || state.history.future.length === 0) {
    return state;
  }

  const next = state.history.future[state.history.future.length - 1];
  const currentSnapshot = snapshotState(state);
  const restored = restoreSnapshot(state, next.snapshot);

  return {
    ...restored,
    history: {
      ...state.history,
      past: [...state.history.past, { label: next.label, snapshot: currentSnapshot }],
      future: state.history.future.slice(0, -1)
    }
  };
}

/**
 * @param {StoreState} state
 * @param {string} label
 * @param {(state: StoreState) => StoreState} recipe
 * @returns {StoreState}
 */
function applyMutation(state, label, recipe) {
  const candidate = recipe(state);
  if (!hasCoreChange(state, candidate)) {
    return state;
  }

  if (state.transaction.active) {
    return {
      ...candidate,
      transaction: {
        ...state.transaction,
        dirty: true
      }
    };
  }

  const withHistory = pushHistoryEntry(state, label);
  return {
    ...candidate,
    history: withHistory.history,
    transaction: state.transaction
  };
}

/**
 * @param {StoreState} state
 * @param {unknown} model
 * @returns {StoreState}
 */
function setModelState(state, model) {
  return {
    ...state,
    model: model == null ? null : deepClone(model)
  };
}

/**
 * @param {StoreState} state
 * @param {unknown} compileResult
 * @returns {StoreState}
 */
function setCompileResultState(state, compileResult) {
  return {
    ...state,
    compileResult: compileResult == null ? null : deepClone(compileResult)
  };
}

/**
 * @param {StoreState} state
 * @param {Partial<SelectionSlice>} selection
 * @returns {StoreState}
 */
function setSelectionState(state, selection) {
  const nextSelection = normalizeSelection({
    selectedRefs: selection.selectedRefs ?? state.selection.selectedRefs,
    selectedNet: selection.selectedNet ?? state.selection.selectedNet,
    selectedPin: selection.selectedPin ?? state.selection.selectedPin
  });

  return {
    ...state,
    selection: nextSelection
  };
}

/**
 * @param {StoreState} state
 * @param {string} ref
 * @param {Partial<SchemetaPlacement>} placement
 * @returns {StoreState}
 */
function moveComponentState(state, ref, placement) {
  if (!state.model || !Array.isArray(state.model.instances)) {
    return state;
  }

  let touched = false;
  const instances = state.model.instances.map((instance) => {
    if (instance.ref !== ref) {
      return instance;
    }

    const currentPlacement = instance.placement ?? { x: null, y: null, rotation: 0, locked: false };
    const nextPlacement = {
      ...currentPlacement,
      x: Number.isFinite(placement.x) ? Math.round(Number(placement.x)) : currentPlacement.x,
      y: Number.isFinite(placement.y) ? Math.round(Number(placement.y)) : currentPlacement.y,
      rotation: Number.isFinite(placement.rotation) ? Number(placement.rotation) : currentPlacement.rotation,
      locked: typeof placement.locked === "boolean" ? placement.locked : currentPlacement.locked
    };

    if (
      nextPlacement.x === currentPlacement.x &&
      nextPlacement.y === currentPlacement.y &&
      nextPlacement.rotation === currentPlacement.rotation &&
      nextPlacement.locked === currentPlacement.locked
    ) {
      return instance;
    }

    touched = true;
    return {
      ...instance,
      placement: nextPlacement
    };
  });

  if (!touched) {
    return state;
  }

  return {
    ...state,
    model: {
      ...state.model,
      instances
    }
  };
}

/**
 * @param {StoreState} state
 * @param {string} text
 * @returns {StoreState}
 */
function applyJsonTextFailureState(state, text) {
  return {
    ...state,
    uiFlags: normalizeUiFlags(
      {
        ...state.uiFlags,
        jsonError: text
      },
      state.uiFlags
    )
  };
}

/**
 * @param {StoreState} state
 * @param {Partial<LifecycleSlice>} lifecycle
 * @returns {StoreState}
 */
function setLifecycleState(state, lifecycle) {
  return {
    ...state,
    lifecycle: normalizeLifecycle(lifecycle, state.lifecycle)
  };
}

/**
 * @param {Partial<StoreState>} [overrides]
 * @returns {StoreState}
 */
export function createInitialState(overrides = {}) {
  const historyLimit = Number.isInteger(overrides.history?.limit) ? overrides.history.limit : DEFAULT_HISTORY_LIMIT;

  return {
    model: overrides.model ? deepClone(overrides.model) : null,
    compileResult: overrides.compileResult ? deepClone(overrides.compileResult) : null,
    selection: normalizeSelection(overrides.selection ?? {}),
    viewport: normalizeViewport(overrides.viewport ?? {}, { scale: 1, panX: 40, panY: 40 }),
    uiFlags: normalizeUiFlags(overrides.uiFlags ?? {}, {
      showLabels: true,
      isolateNet: false,
      isolateComponent: false,
      renderMode: "schematic_stub",
      jsonError: null
    }),
    lifecycle: normalizeLifecycle(overrides.lifecycle ?? {}, {
      isCompiling: false,
      lastError: null,
      lastAction: null
    }),
    history: {
      past: [],
      future: [],
      limit: historyLimit
    },
    transaction: {
      active: false,
      label: null,
      before: null,
      dirty: false
    }
  };
}

/**
 * @param {Partial<StoreState>} [initialState]
 */
export function createSchemetaStore(initialState = {}) {
  /** @type {StoreState} */
  let state = createInitialState(initialState);
  const listeners = new Set();

  /**
   * @param {StoreState} nextState
   * @param {string} action
   */
  function publish(nextState, action) {
    const prevState = state;
    if (nextState === prevState) {
      return;
    }

    state = nextState;
    listeners.forEach((listener) => listener(state, prevState, action));
  }

  const actions = {
    /** @param {unknown} model */
    setModel(model) {
      publish(applyMutation(state, "setModel", (s) => setModelState(s, model)), "setModel");
    },

    /** @param {unknown} compileResult */
    setCompileResult(compileResult) {
      publish(
        applyMutation(state, "setCompileResult", (s) => setCompileResultState(s, compileResult)),
        "setCompileResult"
      );
    },

    /** @param {Partial<SelectionSlice>} selection */
    setSelection(selection) {
      publish(applyMutation(state, "setSelection", (s) => setSelectionState(s, selection)), "setSelection");
    },

    /** @param {string} ref @param {Partial<SchemetaPlacement>} placement */
    moveComponent(ref, placement) {
      publish(applyMutation(state, `moveComponent:${ref}`, (s) => moveComponentState(s, ref, placement ?? {})), "moveComponent");
    },

    /** @param {Partial<ViewportSlice>} viewport */
    setViewport(viewport) {
      publish(
        applyMutation(state, "setViewport", (s) => ({ ...s, viewport: normalizeViewport(viewport ?? {}, s.viewport) })),
        "setViewport"
      );
    },

    /** @param {Partial<UiFlagsSlice>} uiFlags */
    setUiFlags(uiFlags) {
      publish(
        applyMutation(state, "setUiFlags", (s) => ({ ...s, uiFlags: normalizeUiFlags(uiFlags ?? {}, s.uiFlags) })),
        "setUiFlags"
      );
    },

    /** @param {Partial<LifecycleSlice>} lifecycle */
    setLifecycle(lifecycle) {
      publish(setLifecycleState(state, lifecycle ?? {}), "setLifecycle");
    },

    /** @param {string} action */
    beginCompile(action) {
      publish(
        setLifecycleState(state, {
          isCompiling: true,
          lastAction: action || "compile",
          lastError: null
        }),
        "beginCompile"
      );
    },

    /** @param {string} action */
    completeCompile(action) {
      publish(
        setLifecycleState(state, {
          isCompiling: false,
          lastAction: action || state.lifecycle.lastAction,
          lastError: null
        }),
        "completeCompile"
      );
    },

    /** @param {string} action @param {unknown} error */
    failCompile(action, error) {
      const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
      publish(
        setLifecycleState(state, {
          isCompiling: false,
          lastAction: action || state.lifecycle.lastAction,
          lastError: message
        }),
        "failCompile"
      );
    },

    /** @param {string} jsonText */
    applyJsonText(jsonText) {
      try {
        const parsed = JSON.parse(jsonText);
        const next = applyMutation(state, "applyJsonText", (s) => {
          const withModel = setModelState(s, parsed);
          return {
            ...withModel,
            uiFlags: normalizeUiFlags({ ...withModel.uiFlags, jsonError: null }, withModel.uiFlags)
          };
        });
        publish(next, "applyJsonText");
        return { ok: true };
      } catch (error) {
        const message = error instanceof Error ? error.message : "Invalid JSON";
        publish(applyJsonTextFailureState(state, message), "applyJsonTextError");
        return { ok: false, error: message };
      }
    },

    /** @param {string} [label] */
    beginTransaction(label = "transaction") {
      if (state.transaction.active) {
        return false;
      }
      publish(
        {
          ...state,
          transaction: {
            active: true,
            label,
            before: snapshotState(state),
            dirty: false
          }
        },
        "beginTransaction"
      );
      return true;
    },

    commitTransaction() {
      if (!state.transaction.active) {
        return false;
      }

      const tx = state.transaction;
      let nextState = {
        ...state,
        transaction: {
          active: false,
          label: null,
          before: null,
          dirty: false
        }
      };

      if (tx.dirty && tx.before) {
        const nextPast = [...state.history.past, { label: tx.label ?? "transaction", snapshot: tx.before }];
        const overflow = Math.max(0, nextPast.length - state.history.limit);
        nextState = {
          ...nextState,
          history: {
            ...state.history,
            past: overflow > 0 ? nextPast.slice(overflow) : nextPast,
            future: []
          }
        };
      }

      publish(nextState, "commitTransaction");
      return true;
    },

    rollbackTransaction() {
      if (!state.transaction.active) {
        return false;
      }

      const tx = state.transaction;
      const rolledBack = tx.before ? restoreSnapshot(state, tx.before) : state;
      publish(
        {
          ...rolledBack,
          transaction: {
            active: false,
            label: null,
            before: null,
            dirty: false
          }
        },
        "rollbackTransaction"
      );
      return true;
    },

    undo() {
      publish(undoState(state), "undo");
    },

    redo() {
      publish(redoState(state), "redo");
    }
  };

  return {
    getState() {
      return state;
    },

    subscribe(listener) {
      listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    },

    actions
  };
}
