import clone from 'clone';
import dayjs from 'dayjs';
import { Traverse, type TraverseContext } from 'neotraverse/modern';
import { type AmbPlayObject, type DateLike, type JsonPlayObject, type PlayObject, REGEX_ISO8601_LOOSE, type Replace } from './Atomic.ts';
import type { ElementOf, MarkOptional } from 'ts-essentials';
import { type ErrorObject, serializeError } from 'serialize-error';
import type { PlayApiCommonDetailed } from './Api.ts';

interface BlockPath { key: string, parent: string };
type BlockPaths = BlockPath[];

/** We know some nodes will never have data that needs to be transformed
 * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them
 */
const blockedKeys: PropertyKey[] = ['patch', 'inputs', 'payload', 'response', 'error'];
/** We know some paths/nodes will never have data that needs to be transformed
 * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them
 */
const blockedPaths: BlockPaths = [
  {
    parent: 'data',
    key: 'meta'
  },
  {
    parent: 'lifecycle',
    key: 'input'
  }
];

export const shouldBlock = (ctx: TraverseContext): boolean => {
  if (blockedKeys.includes(ctx.key)) {
    return true;
  }
  return blockedPaths.some((x) => {
    let blocked = x.key === ctx.key;
    if (blocked && x.parent !== undefined) {
      blocked = ctx.parent !== undefined && ctx.parent.key === x.parent;
    }
    return blocked;
  });
};

export const asJsonPlayObject = (play: AmbPlayObject<DateLike>): JsonPlayObject => {
  const cloned = clone(play);
  new Traverse(cloned).forEach((ctx, x) => {
    if (shouldBlock(ctx)) {
      ctx.block();
      return;
    }

    if (dayjs.isDayjs(x)) {
      ctx.update(x.toISOString());
    }
  });
  return cloned as unknown as JsonPlayObject;
};

export type SerializablePlaySelect = Replace<MarkOptional<PlayApiCommonDetailed, 'queueStates'>, 'error', ErrorObject> & {queueStates?: Replace<ElementOf<PlayApiCommonDetailed['queueStates']>, 'error', ErrorObject>[]};
export const asSerializablePlaySelect = (data: MarkOptional<PlayApiCommonDetailed, 'queueStates'>): SerializablePlaySelect => {
  const {
    error,
    queueStates = [],
    ...rest
  } = data;

  const qMapped = queueStates.map((x) => {
    const {
      error: e,
      ...restQ
    } = x;
    return {
      ...restQ,
      error: e instanceof Error ? serializeError(e) : e
    }
  });

  return {
    ...rest,
    // @ts-expect-error
    error: error instanceof Error ? serializeError(error) : error,
    queueStates: qMapped
  }
}

export const asPlay = (data: JsonPlayObject | PlayObject): PlayObject => {
  const cloned = clone(data);
  new Traverse(cloned).forEach((ctx, x) => {
    if (shouldBlock(ctx)) {
      ctx.block();
      return;
    }

    if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) {
      ctx.update(dayjs(x), true);
    } /*else if (ctx.key === 'listenRanges') {
      const ranges = x[0].map((y: PlayProgressAmb<string>) => {
        if (y.positionPercent === undefined) {
          return new ListenProgressPositional({ timestamp: dayjs(y.timestamp), position: y.position });
        } else {
          return new ListenProgressTS({ timestamp: dayjs(y.timestamp), positionPercent: y.positionPercent });
        }
      });
      ctx.update(ranges, true);
    }*/
  });
  return cloned as unknown as PlayObject;
};

/**
 * Cheaply convert playobject-like json to playobject with full timestamps etc.
 * 
 * If we know the source is a throw-away object (like from drizzle fromDriver) then we don't need to clone it
 * @param data 
 * @returns 
 */
export const asPlayCheap = (data: JsonPlayObject | PlayObject): PlayObject => {
  new Traverse(data).forEach((ctx, x) => {
    if (shouldBlock(ctx)) {
      ctx.block();
      return;
    }

    if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) {
      ctx.update(dayjs(x), true);
    }
  });
  return data as unknown as PlayObject;
};

export const marshalDayjsToWellKnownString = (ctx: TraverseContext, x: any): void => {
  if (dayjs.isDayjs(x)) {
      ctx.update(`dayjs-x.toISOString()`);
  }
}

export const marshalWellKnownIsoStringToDayjs = (ctx: TraverseContext, x: any): void => {
  if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) {
      ctx.update(dayjs(x.substring(6)), true);
  }
}