import Long from 'long';
import {
  compilePriority,
  compileRetryPolicy,
  decodePriority,
  decompileRetryPolicy,
  extractWorkflowType,
  LoadedDataConverter,
} from '@temporalio/common';
import { encodeUserMetadata, decodeUserMetadata } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
import {
  encodeUnifiedSearchAttributes,
  decodeSearchAttributes,
  decodeTypedSearchAttributes,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Headers } from '@temporalio/common/lib/interceptors';
import {
  decodeArrayFromPayloads,
  decodeMapFromPayloads,
  encodeMapToPayloads,
  encodeToPayloads,
} from '@temporalio/common/lib/internal-non-workflow';
import { temporal } from '@temporalio/proto';
import {
  msOptionalToTs,
  msToTs,
  optionalDateToTs,
  optionalTsToDate,
  optionalTsToMs,
  requiredTsToDate,
} from '@temporalio/common/lib/time';
import {
  CalendarSpec,
  CalendarSpecDescription,
  CompiledScheduleOptions,
  CompiledScheduleUpdateOptions,
  Range,
  ScheduleOptions,
  ScheduleUpdateOptions,
  DayOfWeek,
  DAYS_OF_WEEK,
  Month,
  MONTHS,
  LooseRange,
  ScheduleSpec,
  CompiledScheduleAction,
  ScheduleSpecDescription,
  IntervalSpecDescription,
  ScheduleDescriptionAction,
  ScheduleExecutionActionResult,
  ScheduleExecutionResult,
  ScheduleExecutionStartWorkflowActionResult,
  encodeScheduleOverlapPolicy,
} from './schedule-types';

const [encodeSecond, decodeSecond] = makeCalendarSpecFieldCoders(
  'second',
  (x: number) => (typeof x === 'number' && x >= 0 && x <= 59 ? x : undefined),
  (x: number) => x,
  [{ start: 0, end: 0, step: 0 }], // default to 0
  [{ start: 0, end: 59, step: 1 }]
);

const [encodeMinute, decodeMinue] = makeCalendarSpecFieldCoders(
  'minute',
  (x: number) => (typeof x === 'number' && x >= 0 && x <= 59 ? x : undefined),
  (x: number) => x,
  [{ start: 0, end: 0, step: 0 }], // default to 0
  [{ start: 0, end: 59, step: 1 }]
);

const [encodeHour, decodeHour] = makeCalendarSpecFieldCoders(
  'hour',
  (x: number) => (typeof x === 'number' && x >= 0 && x <= 23 ? x : undefined),
  (x: number) => x,
  [{ start: 0, end: 0, step: 0 }], // default to 0
  [{ start: 0, end: 23, step: 1 }]
);

const [encodeDayOfMonth, decodeDayOfMonth] = makeCalendarSpecFieldCoders(
  'dayOfMonth',
  (x: number) => (typeof x === 'number' && x >= 0 && x <= 31 ? x : undefined),
  (x: number) => x,
  [{ start: 1, end: 31, step: 1 }], // default to *
  [{ start: 1, end: 31, step: 1 }]
);

const [encodeMonth, decodeMonth] = makeCalendarSpecFieldCoders(
  'month',
  function monthNameToNumber(month: Month): number | undefined {
    const index = MONTHS.indexOf(month);
    return index >= 0 ? index + 1 : undefined;
  },
  (month: number) => MONTHS[month - 1],
  [{ start: 1, end: 12, step: 1 }], // default to *
  [{ start: 1, end: 12, step: 1 }]
);

const [encodeYear, decodeYear] = makeCalendarSpecFieldCoders(
  'year',
  (x: number) => (typeof x === 'number' ? x : undefined),
  (x: number) => x,
  [], // default to *
  [] // special case: * for years is encoded as no range at all
);

const [encodeDayOfWeek, decodeDayOfWeek] = makeCalendarSpecFieldCoders(
  'dayOfWeek',
  function dayOfWeekNameToNumber(day: DayOfWeek): number | undefined {
    const index = DAYS_OF_WEEK.indexOf(day);
    return index >= 0 ? index : undefined;
  },
  (day: number) => DAYS_OF_WEEK[day],
  [{ start: 0, end: 6, step: 1 }], // default to *
  [{ start: 0, end: 6, step: 1 }]
);

function makeCalendarSpecFieldCoders<Unit>(
  fieldName: string,
  encodeValueFn: (x: Unit) => number | undefined,
  decodeValueFn: (x: number) => Unit | undefined,
  defaultValue: temporal.api.schedule.v1.IRange[],
  matchAllValue: temporal.api.schedule.v1.IRange[]
) {
  function encoder(
    input: LooseRange<Unit> | LooseRange<Unit>[] | '*' | undefined
  ): temporal.api.schedule.v1.IRange[] | undefined {
    if (input === undefined) return defaultValue;
    if (input === '*') return matchAllValue;

    return (Array.isArray(input) ? input : [input]).map((item) => {
      if (typeof item === 'object' && (item as Range<Unit>).start !== undefined) {
        const range = item as Range<Unit>;
        const start = encodeValueFn(range.start);
        if (start !== undefined) {
          return {
            start,
            end: range.end !== undefined ? encodeValueFn(range.end) ?? start : 1,
            step: typeof range.step === 'number' && range.step > 0 ? range.step : 1,
          };
        }
      }
      if (item !== undefined) {
        const value = encodeValueFn(item as Unit);
        if (value !== undefined) return { start: value, end: value, step: 1 };
      }
      throw new TypeError(`Invalid CalendarSpec component for field ${fieldName}: '${item}' of type '${typeof item}'`);
    });
  }

  function decoder(input: temporal.api.schedule.v1.IRange[] | undefined | null): Range<Unit>[] {
    if (!input) return [];
    return (input as temporal.api.schedule.v1.Range[]).map((pb): Range<Unit> => {
      const start = decodeValueFn(pb.start);
      if (start === undefined) {
        throw new RangeError(`Invalid CalendarSpec component for field ${fieldName}: ${pb.start} is out of bounds`);
      }
      const end = pb.end > pb.start ? decodeValueFn(pb.end) ?? start : start;
      const step = pb.step > 0 ? pb.step : 1;
      return { start, end, step };
    });
  }

  return [encoder, decoder] as const;
}

export function encodeOptionalStructuredCalendarSpecs(
  input: CalendarSpec[] | null | undefined
): temporal.api.schedule.v1.IStructuredCalendarSpec[] | undefined {
  if (!input) return undefined;
  return input.map((spec) => ({
    second: encodeSecond(spec.second),
    minute: encodeMinute(spec.minute),
    hour: encodeHour(spec.hour),
    dayOfMonth: encodeDayOfMonth(spec.dayOfMonth),
    month: encodeMonth(spec.month),
    year: encodeYear(spec.year),
    dayOfWeek: encodeDayOfWeek(spec.dayOfWeek),
    comment: spec.comment,
  }));
}

export function decodeOptionalStructuredCalendarSpecs(
  input: temporal.api.schedule.v1.IStructuredCalendarSpec[] | null | undefined
): CalendarSpecDescription[] {
  if (!input) return [];

  return (input as temporal.api.schedule.v1.StructuredCalendarSpec[]).map(
    (pb): CalendarSpecDescription => ({
      second: decodeSecond(pb.second),
      minute: decodeMinue(pb.minute),
      hour: decodeHour(pb.hour),
      dayOfMonth: decodeDayOfMonth(pb.dayOfMonth),
      month: decodeMonth(pb.month),
      year: decodeYear(pb.year),
      dayOfWeek: decodeDayOfWeek(pb.dayOfWeek),
      comment: pb.comment,
    })
  );
}

export function compileScheduleOptions(options: ScheduleOptions): CompiledScheduleOptions {
  const workflowType = extractWorkflowType(options.action.workflowType);
  return {
    ...options,
    action: {
      ...options.action,
      workflowId: options.action.workflowId ?? `${options.scheduleId}-workflow`,
      workflowType,
      args: (options.action.args ?? []) as unknown[],
    },
  };
}

export function compileUpdatedScheduleOptions(
  scheduleId: string,
  options: ScheduleUpdateOptions
): CompiledScheduleUpdateOptions {
  const workflowTypeOrFunc = options.action.workflowType;
  const workflowType = extractWorkflowType(workflowTypeOrFunc);
  return {
    ...options,
    action: {
      ...options.action,
      workflowId: options.action.workflowId ?? `${scheduleId}-workflow`,
      workflowType,
      args: (options.action.args ?? []) as unknown[],
    },
  };
}

export function encodeScheduleSpec(spec: ScheduleSpec): temporal.api.schedule.v1.IScheduleSpec {
  return {
    structuredCalendar: encodeOptionalStructuredCalendarSpecs(spec.calendars),
    interval: spec.intervals?.map((interval) => ({
      interval: msToTs(interval.every),
      phase: msOptionalToTs(interval.offset),
    })),
    cronString: spec.cronExpressions,
    excludeStructuredCalendar: encodeOptionalStructuredCalendarSpecs(spec.skip),
    startTime: optionalDateToTs(spec.startAt),
    endTime: optionalDateToTs(spec.endAt),
    jitter: msOptionalToTs(spec.jitter),
    timezoneName: spec.timezone,
  };
}

export async function encodeScheduleAction(
  dataConverter: LoadedDataConverter,
  action: CompiledScheduleAction,
  headers: Headers
): Promise<temporal.api.schedule.v1.IScheduleAction> {
  return {
    startWorkflow: {
      workflowId: action.workflowId,
      workflowType: {
        name: action.workflowType,
      },
      input: { payloads: await encodeToPayloads(dataConverter, ...action.args) },
      taskQueue: {
        kind: temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL,
        name: action.taskQueue,
      },
      workflowExecutionTimeout: msOptionalToTs(action.workflowExecutionTimeout),
      workflowRunTimeout: msOptionalToTs(action.workflowRunTimeout),
      workflowTaskTimeout: msOptionalToTs(action.workflowTaskTimeout),
      retryPolicy: action.retry ? compileRetryPolicy(action.retry) : undefined,
      memo: action.memo ? { fields: await encodeMapToPayloads(dataConverter, action.memo) } : undefined,
      searchAttributes:
        action.searchAttributes || action.typedSearchAttributes // eslint-disable-line @typescript-eslint/no-deprecated
          ? {
              indexedFields: encodeUnifiedSearchAttributes(action.searchAttributes, action.typedSearchAttributes), // eslint-disable-line @typescript-eslint/no-deprecated
            }
          : undefined,
      header: { fields: headers },
      userMetadata: await encodeUserMetadata(dataConverter, action.staticSummary, action.staticDetails),
      priority: action.priority ? compilePriority(action.priority) : undefined,
    },
  };
}

export function encodeSchedulePolicies(
  policies?: ScheduleOptions['policies']
): temporal.api.schedule.v1.ISchedulePolicies {
  return {
    catchupWindow: msOptionalToTs(policies?.catchupWindow),
    overlapPolicy: policies?.overlap ? encodeScheduleOverlapPolicy(policies.overlap) : undefined,
    pauseOnFailure: policies?.pauseOnFailure,
  };
}

export function encodeScheduleState(state?: ScheduleOptions['state']): temporal.api.schedule.v1.IScheduleState {
  return {
    paused: state?.paused,
    notes: state?.note,
    limitedActions: state?.remainingActions !== undefined,
    remainingActions: state?.remainingActions ? Long.fromNumber(state?.remainingActions) : undefined,
  };
}

export function decodeScheduleSpec(pb: temporal.api.schedule.v1.IScheduleSpec): ScheduleSpecDescription {
  // Note: the server will have compiled calendar and cron_string fields into
  // structured_calendar (and maybe interval and timezone_name), so at this
  // point, we'll see only structured_calendar, interval, etc.
  return {
    calendars: decodeOptionalStructuredCalendarSpecs(pb.structuredCalendar),
    intervals: (pb.interval ?? []).map(
      (x) =>
        <IntervalSpecDescription>{
          every: optionalTsToMs(x.interval),
          offset: optionalTsToMs(x.phase),
        }
    ),
    skip: decodeOptionalStructuredCalendarSpecs(pb.excludeStructuredCalendar),
    startAt: optionalTsToDate(pb.startTime),
    endAt: optionalTsToDate(pb.endTime),
    jitter: optionalTsToMs(pb.jitter),
    timezone: pb.timezoneName ?? undefined,
  };
}

export async function decodeScheduleAction(
  dataConverter: LoadedDataConverter,
  pb: temporal.api.schedule.v1.IScheduleAction
): Promise<ScheduleDescriptionAction> {
  if (pb.startWorkflow) {
    const { staticSummary, staticDetails } = await decodeUserMetadata(dataConverter, pb.startWorkflow?.userMetadata);
    return {
      type: 'startWorkflow',

      workflowId: pb.startWorkflow.workflowId!,

      workflowType: pb.startWorkflow.workflowType!.name!,

      taskQueue: pb.startWorkflow.taskQueue!.name!,
      args: await decodeArrayFromPayloads(dataConverter, pb.startWorkflow.input?.payloads),
      memo: await decodeMapFromPayloads(dataConverter, pb.startWorkflow.memo?.fields),
      retry: decompileRetryPolicy(pb.startWorkflow.retryPolicy),
      searchAttributes: decodeSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
      typedSearchAttributes: decodeTypedSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
      workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout),
      workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout),
      workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout),
      staticSummary,
      staticDetails,
      priority: decodePriority(pb.startWorkflow.priority),
    };
  }
  throw new TypeError('Unsupported schedule action');
}

export function decodeScheduleRunningActions(
  pb?: temporal.api.common.v1.IWorkflowExecution[] | null
): ScheduleExecutionStartWorkflowActionResult[] {
  if (!pb) return [];
  return pb.map(
    (x): ScheduleExecutionStartWorkflowActionResult => ({
      type: 'startWorkflow',
      workflow: {
        workflowId: x.workflowId!,

        firstExecutionRunId: x.runId!,
      },
    })
  );
}

export function decodeScheduleRecentActions(
  pb?: temporal.api.schedule.v1.IScheduleActionResult[] | null
): ScheduleExecutionResult[] {
  if (!pb) return [];
  return (pb as Required<temporal.api.schedule.v1.IScheduleActionResult>[]).map(
    (executionResult): ScheduleExecutionResult => {
      let action: ScheduleExecutionActionResult | undefined;
      if (executionResult.startWorkflowResult) {
        action = {
          type: 'startWorkflow',
          workflow: {
            workflowId: executionResult.startWorkflowResult!.workflowId!,

            firstExecutionRunId: executionResult.startWorkflowResult!.runId!,
          },
        };
      } else throw new TypeError('Unsupported schedule action');

      return {
        scheduledAt: requiredTsToDate(executionResult.scheduleTime, 'scheduleTime'),
        takenAt: requiredTsToDate(executionResult.actualTime, 'actualTime'),
        action,
      };
    }
  );
}
