import { isIssue, Issue, IssueResult, Next, Path, Result, ValueProcessor } from './types';

export interface WithDefault<T> {
  default?: T | (() => T);
}

export interface MaybeOptions {
  incorrectTypeToUndefined?: boolean;
  strictParsing?: boolean;
}

export type UndefinedHandler<T> = () => Result<T> | undefined;
export type Definitely<T> = (next: Next<unknown, T>) => (value: unknown, path?: Path[]) => Result<T>;
export type IsAs<T> = (next: Next<T, T>) => (value: unknown, path: Path[]) => Result<T>;
export type Maybe<T> = (next: Next<unknown, T>) => (value: unknown, path?: Path[]) => Result<T | undefined>;
export type Empty = (value: unknown) => boolean;
export type Check<T> = (value: unknown) => value is T;
export type Convert<T, O = CommonConvertOptions<T>> = (value: unknown, options?: O) => T | undefined;
export type Coerce<T, O> = (options?: O) => (next: Next<T, T>) => (value: T, path: Path[]) => Result<T>;
export type Validate<T, O> = (value: T, path: Path[], options: O) => IssueResult;

export const withDefault = <T>(options?: WithDefault<T>): UndefinedHandler<T> => (): Result<T> | undefined => {
  if (options?.default === undefined) return undefined;
  return { value: options?.default instanceof Function ? options.default() : options.default };
};

export const definitely = <T>(undefinedHandler?: UndefinedHandler<T>): Definitely<T> => (next) => (value, path = []) => {
  if (value === null || value === undefined) {
    return undefinedHandler?.() ?? { issues: [Issue.forPath(path, value, 'not-defined')] };
  }
  return next(value, path);
};

export const nullOrUndefined: Empty = (value) => {
  return value === null || value === undefined;
};

export const maybe = <T>(empty: Empty, check: Check<T>, options?: MaybeOptions, undefinedHandler?: UndefinedHandler<T>): Maybe<T> => (next) => (value, path = []) => {
  if (empty(value)) {
    return undefinedHandler?.() ?? { value: undefined };
  }

  if (options?.incorrectTypeToUndefined) {
    if (!check(value)) {
      return { value: undefined };
    }
  }

  const result = next(value, path);
  if (isIssue(result) && result.issues.length === 1 && result.issues[0].reason === 'no-conversion') {
    if (options?.strictParsing) return result;
    return { value: undefined };
  }
  return result;
};

export const is = <T>(check: Check<T>, typeName: string): IsAs<T> => (next) => (value, path = []) => {
  if (!check(value)) {
    return { issues: [Issue.forPath(path, value, 'incorrect-type', { expectedType: typeName })] };
  }
  return next(value, path);
};

export const as = <T, O extends CommonConvertOptions<T>>(check: Check<T>, convert: Convert<T, O>, typeName: string, undefinedHandler?: UndefinedHandler<T>, options?: O): IsAs<T> => (next) => (value, path = []) => {
  if (check(value)) return next(value, path);

  const converted = options?.converter?.(value, options.convertOptions) ?? convert(value, options);
  if (converted === undefined || converted === null) {
    return undefinedHandler?.() ?? { issues: [Issue.forPath(path, value, 'no-conversion', { toType: typeName })] };
  }
  return next(converted, path);
};

const getResultOrValidationIssues = <T, O extends CommonValidationOptions<T>>(validate: Validate<T, O>, value: T, path: Path[], options?: O): Result<T> => {
  if (!options) return { value };
  const validationResult = validate(value, path, options);
  return validationResult.issues.length ? validationResult : { value };
};

export interface CommonValidationOptions<T> {
  validator?: (value: T, options?: any, path?: Path[]) => boolean | Issue[];
  validatorOptions?: any;
}

export interface CommonConvertOptions<T> {
  converter?: (value: unknown, options?: any) => T | undefined;
  convertOptions?: any;
}

export const isNullable = <T>(processor: ValueProcessor<T>): ValueProcessor<T | null> => ({
  process: (value: unknown, path?: Path[]): Result<T | null> => {
    if (value === null) return { value: null };

    return processor.process(value, path);
  },
});

export const asNullable = <T>(processor: ValueProcessor<T>, options?: WithDefault<Exclude<T, undefined> | null>): ValueProcessor<Exclude<T, undefined> | null> => ({
  process: (value: unknown, path?: Path[]): Result<Exclude<T, undefined> | null> => {
    if (value === null) return { value: null };

    const result = processor.process(value, path);
    if (!isIssue(result) && result.value === undefined) {
      const defaultValue = options?.default === undefined
        ? null
        : options?.default instanceof Function
          ? options.default()
          : options.default;
      return { value: defaultValue };
    }
    return result as Result<Exclude<T, undefined>>;
  },
});

export const createIsCheck = <T, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
  typeName: string,
  check: Check<T>,
  coerce: Coerce<T, TCoerceOptions>,
  validate: Validate<T, TValidationOptions>,
) => (options?: TCoerceOptions & TValidationOptions): ValueProcessor<T> => {
  return {
    process: definitely<T>()(
      is(check, typeName)(
        coerce(options)(
          (value, path) => getResultOrValidationIssues(validate, value, path, options)
        )
      )
    ),
  };
};

export const createMaybeCheck = <T, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
  typeName: string,
  check: Check<T>,
  coerce: Coerce<T, TCoerceOptions>,
  validate: Validate<T, TValidationOptions>,
  empty = nullOrUndefined,
) => (options?: MaybeOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T | undefined> => {
  return {
    process: maybe(empty, check, options)(
      is(check, typeName)(
        coerce(options)(
          (value, path) => getResultOrValidationIssues(validate, value, path, options)
        )
      )
    ),
  };
};

export const createAsCheck = <T, TConvertOptions extends CommonConvertOptions<T>, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
  typeName: string,
  check: Check<T>,
  convert: Convert<T, TConvertOptions>,
  coerce: Coerce<T, TCoerceOptions>,
  validate: Validate<T, TValidationOptions>,
) => (options?: WithDefault<T> & TConvertOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T> => {
  return {
    process: definitely<T>(withDefault(options))(
      as(check, convert, typeName, withDefault(options), options)(
        coerce(options)(
          (value, path) => getResultOrValidationIssues(validate, value, path, options)
        )
      )
    ),
  };
};

export const createMaybeAsCheck = <T, TConvertOptions extends CommonConvertOptions<T>, TCoerceOptions, TValidationOptions extends CommonValidationOptions<T>>(
  typeName: string,
  check: Check<T>,
  convert: Convert<T, TConvertOptions>,
  coerce: Coerce<T, TCoerceOptions>,
  validate: Validate<T, TValidationOptions>,
  empty = nullOrUndefined,
) => (options?: MaybeOptions & WithDefault<T> & TConvertOptions & TCoerceOptions & TValidationOptions): ValueProcessor<T | undefined> => {
  return {
    process: maybe(empty, check, options, withDefault(options))(
      as(check, convert, typeName, withDefault(options), options)(
        coerce(options)(
          (value, path) => getResultOrValidationIssues(validate, value, path, options)
        )
      ),
    ),
  };
};

export const basicValidation = <T>(value: T, path: Path[], options: CommonValidationOptions<T>): IssueResult => {
  const customValidatorResult = options.validator?.(value, options.validatorOptions, path);
  if (customValidatorResult === undefined || customValidatorResult === true) return { issues: [] };
  if (customValidatorResult === false) return { issues: [Issue.forPath(path, value, 'validator')] };
  return { issues: customValidatorResult };
};
