import { isSessionGenerationCurrent, type ISessionState } from './classes.sessionstate.js';

export interface ILatestIntentToken {
  scope: string;
  requestKey: string;
  generation: number;
  sequence: number;
}

export class LatestIntentTracker {
  private sequence = 0;
  private latestByScope = new Map<string, ILatestIntentToken>();

  public begin(
    scopeArg: string,
    requestKeyArg: string,
    generationArg: number,
    coalescingArg: boolean,
  ): ILatestIntentToken {
    const latest = this.latestByScope.get(scopeArg);
    if (
      coalescingArg
      && latest?.generation === generationArg
      && latest.requestKey === requestKeyArg
    ) {
      return latest;
    }
    const token: ILatestIntentToken = {
      scope: scopeArg,
      requestKey: requestKeyArg,
      generation: generationArg,
      sequence: ++this.sequence,
    };
    this.latestByScope.set(scopeArg, token);
    return token;
  }

  public isCurrent(tokenArg: ILatestIntentToken): boolean {
    const latest = this.latestByScope.get(tokenArg.scope);
    return latest?.generation === tokenArg.generation
      && latest.requestKey === tokenArg.requestKey;
  }
}

export class MutationDomainScheduler {
  private tails = new Map<string, Promise<void>>();

  public run<TResult>(
    domainArg: string,
    taskArg: () => Promise<TResult>,
  ): Promise<TResult> {
    const previous = this.tails.get(domainArg) ?? Promise.resolve();
    const result = previous.then(taskArg, taskArg);
    const tail = result.then(
      () => undefined,
      () => undefined,
    );
    this.tails.set(domainArg, tail);
    void tail.then(() => {
      if (this.tails.get(domainArg) === tail) {
        this.tails.delete(domainArg);
      }
    });
    return result;
  }
}

const entityIdentityKeys = [
  'id', 'operationId', 'upgradeId', 'name', 'serviceName', 'serviceType',
  'domain', 'key', 'tokenId', 'token', 'type',
] as const;

const stableSerialize = (valueArg: unknown, seenArg = new WeakSet<object>()): string => {
  if (valueArg === null) return 'null';
  if (typeof valueArg !== 'object') return `${typeof valueArg}:${String(valueArg)}`;
  if (seenArg.has(valueArg)) return 'object:[Circular]';
  seenArg.add(valueArg);
  let serialized: string;
  if (Array.isArray(valueArg)) {
    serialized = `array:[${valueArg.map((value) => stableSerialize(value, seenArg)).join(',')}]`;
  } else {
    const record = valueArg as Record<string, unknown>;
    serialized = `object:{${Object.keys(record)
      .sort()
      .map((key) => `${key}:${stableSerialize(record[key], seenArg)}`)
      .join(',')}}`;
  }
  seenArg.delete(valueArg);
  return serialized;
};

const valuesEquivalent = (leftArg: unknown, rightArg: unknown): boolean =>
  Object.is(leftArg, rightArg) || stableSerialize(leftArg) === stableSerialize(rightArg);

const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
  Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);

const getEntityIdentity = (valueArg: unknown): string => {
  if (!isRecord(valueArg)) return stableSerialize(valueArg);
  if (
    typeof valueArg.serviceId === 'number'
    && typeof valueArg.timestamp === 'number'
    && typeof valueArg.message === 'string'
    && (valueArg.source === 'stdout' || valueArg.source === 'stderr')
  ) {
    return `log:${stableSerialize([
      valueArg.serviceId,
      valueArg.timestamp,
      valueArg.message,
      valueArg.source,
    ])}`;
  }
  for (const key of entityIdentityKeys) {
    const identityValue = valueArg[key];
    if (
      identityValue !== undefined
      && identityValue !== null
      && typeof identityValue !== 'object'
    ) {
      return `${key}:${typeof identityValue}:${String(identityValue)}`;
    }
  }
  return stableSerialize(valueArg);
};

const indexEntities = <TValue>(valuesArg: TValue[]): Map<string, TValue> => {
  const occurrences = new Map<string, number>();
  const indexed = new Map<string, TValue>();
  for (const value of valuesArg) {
    const identity = getEntityIdentity(value);
    const occurrence = occurrences.get(identity) ?? 0;
    occurrences.set(identity, occurrence + 1);
    indexed.set(`${identity}#${occurrence}`, value);
  }
  return indexed;
};

const mergeRecordState = (
  baseStateArg: Record<string, unknown>,
  actionStateArg: Record<string, unknown>,
  latestStateArg: Record<string, unknown>,
): Record<string, unknown> => {
  const merged = { ...latestStateArg };
  const keys = new Set([...Object.keys(baseStateArg), ...Object.keys(actionStateArg)]);
  for (const key of keys) {
    const baseValue = baseStateArg[key];
    const actionValue = actionStateArg[key];
    if (valuesEquivalent(baseValue, actionValue)) continue;
    if (!Object.hasOwn(actionStateArg, key)) {
      delete merged[key];
      continue;
    }
    const latestValue = latestStateArg[key];
    if (
      Array.isArray(baseValue)
      && Array.isArray(actionValue)
      && Array.isArray(latestValue)
    ) {
      merged[key] = mergeEntityArray(baseValue, actionValue, latestValue);
    } else if (
      isRecord(baseValue)
      && isRecord(actionValue)
      && isRecord(latestValue)
    ) {
      merged[key] = mergeRecordState(baseValue, actionValue, latestValue);
    } else {
      merged[key] = actionValue;
    }
  }
  return merged;
};

const mergeEntityArray = <TValue>(
  baseValuesArg: TValue[],
  actionValuesArg: TValue[],
  latestValuesArg: TValue[],
): TValue[] => {
  const baseByIdentity = indexEntities(baseValuesArg);
  const actionByIdentity = indexEntities(actionValuesArg);
  const latestByIdentity = indexEntities(latestValuesArg);
  const removedIdentities = new Set<string>();
  for (const identity of baseByIdentity.keys()) {
    if (!actionByIdentity.has(identity)) removedIdentities.add(identity);
  }

  const mergedByIdentity = new Map(latestByIdentity);
  for (const identity of removedIdentities) {
    mergedByIdentity.delete(identity);
  }
  for (const [identity, actionValue] of actionByIdentity) {
    const baseHasIdentity = baseByIdentity.has(identity);
    const latestHasIdentity = mergedByIdentity.has(identity);
    if (baseHasIdentity) {
      const baseValue = baseByIdentity.get(identity) as TValue;
      if (valuesEquivalent(baseValue, actionValue)) continue;
      if (!latestHasIdentity) continue;
      const latestValue = mergedByIdentity.get(identity) as TValue;
      mergedByIdentity.set(
        identity,
        isRecord(baseValue) && isRecord(actionValue) && isRecord(latestValue)
          ? mergeRecordState(baseValue, actionValue, latestValue) as TValue
          : actionValue,
      );
      continue;
    }
    if (latestHasIdentity) {
      const latestValue = mergedByIdentity.get(identity) as TValue;
      mergedByIdentity.set(
        identity,
        isRecord(actionValue) && isRecord(latestValue)
          ? mergeRecordState({}, actionValue, latestValue) as TValue
          : actionValue,
      );
    } else {
      mergedByIdentity.set(identity, actionValue);
    }
  }
  return [...mergedByIdentity.values()];
};

export const mergeActionState = <TState extends object>(
  baseStateArg: TState,
  actionStateArg: TState,
  latestStateArg: TState,
): TState => mergeRecordState(
  baseStateArg as Record<string, unknown>,
  actionStateArg as Record<string, unknown>,
  latestStateArg as Record<string, unknown>,
) as TState;

export const isProtectedRequestCurrent = (
  sessionArg: ISessionState,
  requestGenerationArg: number,
  schedulerGenerationArg: number,
  intentCurrentArg: boolean,
  nowArg = Date.now(),
): boolean => intentCurrentArg
  && requestGenerationArg === schedulerGenerationArg
  && isSessionGenerationCurrent(sessionArg, requestGenerationArg, nowArg);
