import type { Selectable, Transaction } from "@tailor-platform/sdk/kysely";
import { ok, err, type Result } from "./result";

interface TransitionConfig<TState extends string> {
  from: readonly TState[];
  to: TState;
}

type TransitionsMap<TState extends string> = Record<string, TransitionConfig<TState>>;

type StateOf<T extends TransitionsMap<string>> = T[keyof T]["from"][number] | T[keyof T]["to"];

export interface Lifecycle<T extends TransitionsMap<string>> {
  readonly states: readonly [StateOf<T>, ...StateOf<T>[]];
  readonly transitions: Readonly<T>;
  tryTransition<K extends keyof T & string>(
    currentState: StateOf<T>,
    transition: K,
  ): T[K]["to"] | undefined;
}

/** Define a state machine from a transitions map. */
export function defineLifecycle<const T extends TransitionsMap<string>>(
  transitions: T,
): Lifecycle<T> {
  const stateSet = new Set<string>();
  for (const t of Object.values(transitions)) {
    for (const f of t.from) stateSet.add(f);
    stateSet.add(t.to);
  }

  return {
    states: [...stateSet] as [StateOf<T>, ...StateOf<T>[]],
    transitions,
    tryTransition(currentState, transition) {
      const t = transitions[transition];
      if (!t) return undefined;
      return t.from.includes(currentState) ? t.to : undefined;
    },
  };
}

export interface TransitionErrors<NF extends Error, IT extends Error> {
  notFound: new (id: string) => NF;
  invalidTransition: new (id: string) => IT;
}

/** Execute a status transition: fetch with forUpdate, validate, then update. */
export async function executeTransition<
  DB,
  TN extends keyof DB & string,
  SF extends keyof DB[TN] & string,
  T extends TransitionsMap<DB[TN][SF] & string>,
  NF extends Error,
  IT extends Error,
>(params: {
  db: Transaction<DB>;
  tableName: TN;
  statusField: SF;
  id: string;
  transition: keyof T & string;
  lifecycle: Lifecycle<T>;
  errors: TransitionErrors<NF, IT>;
}): Promise<Result<Selectable<DB[TN]>, NF | IT>> {
  const { db, tableName, statusField, id, transition, lifecycle, errors } = params;
  // Kysely's conditional types don't resolve with generic type parameters
  // oxlint-disable-next-line typescript/no-explicit-any
  const trx = db as Transaction<any>;
  const table = tableName as string;
  const field = statusField as string;

  const entity = await trx
    .selectFrom(table)
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!entity) {
    return err(new errors.notFound(id));
  }

  const currentState = (entity as Record<string, unknown>)[field] as StateOf<T>;
  const targetState = lifecycle.tryTransition(currentState, transition);
  if (!targetState) {
    return err(new errors.invalidTransition(id));
  }

  const updated = await trx
    .updateTable(table)
    .set({ [field]: targetState })
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok(updated as Selectable<DB[TN]>);
}
