import { VM } from "../artefacts/vapi/index.js";
import { Duration, MachineAddress, enforce_nonnull, parse_machine_address } from "vscript";
import { env_keys, getenv } from "./node/polyfills.js";

class MissingParameter {}

export class ParseFailure {}

// throws on failure
type Parser<T> = (env_val: string | undefined, machines: VM.Any[]) => Promise<T>;

type Parspec<T> = {
  parser: Parser<T>;
  description: () => string;
};

type ParameterDefault<T> = T | ((machines: VM.Any[]) => T) | ((machines: VM.Any[]) => Promise<T>);

function make_bool_parspec(default_value?: ParameterDefault<boolean>): Parspec<boolean> {
  return {
    parser: parse_bool(default_value),
    description: () => "bool ('true' or 'false')",
  };
}

async function reify_default<T>(
  machines: VM.Any[],
  maybe_default?: ParameterDefault<T>,
): Promise<T | undefined> {
  if (maybe_default === undefined) return undefined;
  if (typeof maybe_default === "function") {
    const tmp = (maybe_default as any)(machines);
    if (typeof tmp.then === "function") {
      return await tmp;
    } else {
      return tmp;
    }
  }
  return maybe_default;
}

function make_number_parspec(
  num_type: "int" | "float",
  pars?: { min?: number; max?: number; default?: ParameterDefault<number> },
): Parspec<number> {
  return {
    parser: parse_number(num_type, pars),
    description: () => {
      const suffix = (() => {
        if (pars?.min !== undefined && pars?.max !== undefined) {
          return ` between ${pars.min} and ${pars.max} (inclusive)`;
        } else if (pars?.min !== undefined) {
          return ` >= ${pars.min}`;
        } else if (pars?.max !== undefined) {
          return ` <= ${pars.max}`;
        }
        return "";
      })();
      return `${num_type}${suffix}`;
    },
  };
}

function make_string_parspec(default_value?: ParameterDefault<string>): Parspec<string> {
  return {
    parser: parse_string(default_value),
    description: () => "string",
  };
}

function make_enum_parspec<T extends string>(
  enumerators: T[],
  default_value?: ParameterDefault<T>,
): Parspec<T> {
  return {
    parser: parse_enum({ enumerators, default: default_value }),
    description: () => `one of the following: ${enumerators.join(", ")}`,
  };
}

function make_list_parspec<T>(
  element_parspec: Parspec<T>,
  default_value?: ParameterDefault<T[]>,
): Parspec<T[]> {
  return {
    parser: parse_list(element_parspec.parser, default_value),
    description: () => `a comma-separated list of [${element_parspec.description()}]`,
  };
}

function make_optional_parspec<T>(inner_parspec: Parspec<T>): Parspec<T | null> {
  return {
    description: () => inner_parspec.description() + " (optional)",
    parser: async (env_val: string | undefined, machines: VM.Any[]) => {
      if (env_val === undefined) return null;
      return await inner_parspec.parser(env_val, machines);
    },
  };
}

export type Parspecs = {
  [parname: string]: Parspec<any>;
};

type ParameterValue<P> = P extends Parspec<infer T> ? ParameterDefault<T> : never;

function wrap_missing<T>(
  inner_parser: (env_val: string, machines: VM.Any[]) => Promise<T>,
  default_value?: ParameterDefault<T>,
): Parser<T> {
  return async (env_val: string | undefined, machines: VM.Any[]) => {
    if (env_val === undefined) {
      if (default_value !== undefined)
        return enforce_nonnull(await reify_default(machines, default_value));
      throw new MissingParameter();
    }
    return await inner_parser(env_val, machines);
  };
}

function parse_bool(default_value?: ParameterDefault<boolean>): Parser<boolean> {
  return wrap_missing(async (env_val: string) => {
    switch ((env_val ?? "").trim().toLowerCase()) {
      case "false":
        return false;
      case "true":
        return true;
    }
    throw new ParseFailure();
  }, default_value);
}

function parse_number_ll(
  env_val: string,
  num_type: "float" | "int",
  pars?: { min?: number; max?: number },
): number {
  const i = (num_type === "int" ? parseInt : parseFloat)(env_val.trim(), 10);
  if (
    isNaN(i) ||
    (pars?.min !== undefined && i < pars?.min) ||
    (pars?.max !== undefined && i > pars?.max)
  ) {
    throw new ParseFailure();
  }
  return i;
}

function parse_number(
  num_type: "float" | "int",
  pars?: { min?: number; max?: number; default?: ParameterDefault<number> },
): Parser<number> {
  return wrap_missing(
    async (env_val: string) => {
      return parse_number_ll(env_val, num_type, pars);
    },
    pars?.default,
  );
}

function parse_enum_ll<T extends string>(env_val: string, enumerators: T[]): T {
  // we're using this for ENV parsing so we're very tolerant when it
  // comes to capitalization
  const val_lower = env_val.toLowerCase();
  for (const choice of enumerators) {
    if (choice.trim().toLowerCase() === val_lower) return choice;
  }
  throw new ParseFailure();
}

function parse_enum<T extends string>(pars: {
  enumerators: T[];
  default?: ParameterDefault<T>;
}): Parser<T> {
  return wrap_missing(async (env_val: string) => {
    return parse_enum_ll(env_val, pars.enumerators);
  }, pars.default);
}

function parse_string(default_value?: ParameterDefault<string>): Parser<string> {
  return wrap_missing(async (env_val: string) => env_val, default_value);
}

function parse_list<T>(
  element_parser: Parser<T>,
  default_value?: ParameterDefault<T[]>,
): Parser<T[]> {
  return wrap_missing(async (env_val: string, machines: VM.Any[]) => {
    return await Promise.all(env_val.split(",").map((s) => element_parser(s, machines)));
  }, default_value);
}

function make_machine_address_parspec(): Parspec<MachineAddress> {
  return {
    parser: wrap_missing(async (str: string) => {
      try {
        return parse_machine_address(str);
      } catch (_) {
        throw new ParseFailure();
      }
    }, undefined),
    description: () =>
      "machine address (examples are 172.16.1.23, ws://192.168.1.45 or wss://172.16.100.0)",
  };
}

function make_duration_parspec(pars?: {
  min?: Duration;
  max?: Duration;
  default?: ParameterDefault<Duration>;
}): Parspec<Duration> {
  return {
    parser: wrap_missing(
      async (env_val: string, _machines: VM.Any[]) => {
        const m = env_val.match(/([0-9]*(?:\.[0-9]*)?)(s|ms|us|µs|ns|h|min)/);
        if (!m) throw new ParseFailure();
        const value = parseFloat(enforce_nonnull(m[1]));
        if (isNaN(value)) throw new ParseFailure();
        let dur: Duration;
        switch (m[2]) {
          case "s":
          case "ms":
          case "us":
          case "µs":
          case "ns":
          case "h":
          case "min":
            dur = new Duration(value, m[2]);
            break;
          default:
            throw new ParseFailure();
        }
        if (
          (pars?.min !== undefined && dur.s() < pars?.min.s()) ||
          (pars?.max !== undefined && dur.s() > pars?.max.s())
        )
          throw new ParseFailure();
        return dur;
      },
      pars?.default,
    ),
    description: () => {
      if (pars?.min !== undefined && pars?.max !== undefined) {
        return `Duration between ${pars.min} and ${pars.max} (inclusive)`;
      } else if (pars?.min !== undefined) {
        return `Duration >= ${pars.min}`;
      } else if (pars?.max !== undefined) {
        return `Duration <= ${pars.max}`;
      }
      return "Duration";
    },
  };
}

function make_first_of_parspec<T, S>(
  a: Parspec<T>,
  b: Parspec<S>,
  maybe_default?: ParameterDefault<T | S>,
): Parspec<T | S> {
  const description = () => `either (${a.description()}) or (${b.description()})`;
  return {
    description,
    parser: async (env_val: string | undefined, machines: VM.Any[]) => {
      if (env_val === undefined) {
        const default_value = await reify_default(machines, maybe_default);
        if (default_value === undefined)
          throw new Error(
            `Missing parameter: please specify ${description()} or provide a default value`,
          );
        return default_value;
      }
      let error_a = "";
      try {
        return await a.parser(env_val, machines);
      } catch (e) {
        error_a = `${e}`;
      }
      try {
        return await b.parser(env_val, machines);
      } catch (e) {
        throw new Error(
          `Unable to parse ${env_val} as ${description()}: trying to parse as ${a.description()} failed with '${error_a.trim()}', while trying to parse as ${b.description()} failed with '${`${e}`.trim()}'`,
        );
      }
    },
  };
}

export const ENV = {
  bool: make_bool_parspec,
  true: make_bool_parspec(true),
  false: make_bool_parspec(false),
  string: make_string_parspec,
  int: (pars?: { min?: number; max?: number; default?: ParameterDefault<number> }) =>
    make_number_parspec("int", pars),
  float: (pars?: { min?: number; max?: number; default?: ParameterDefault<number> }) =>
    make_number_parspec("float", pars),
  enum: make_enum_parspec,
  duration: make_duration_parspec,
  list_of: make_list_parspec,
  custom: <T>(ps: Parspec<T>) => ps,
  machine_address: make_machine_address_parspec(),
  optional: make_optional_parspec,
  first_of: make_first_of_parspec,
} as const;

export type ParameterValues<P extends Parspecs> = {
  [parname in keyof P]: ParameterValue<P[parname]>;
};

export type ReifiedParameterValues<P extends Parspecs> = {
  [parname in keyof P]: P[parname] extends Parspec<infer T> ? T : never;
};

export type ParameterDefaults<P extends Parspecs> = Partial<{
  [parname in keyof P]: P[parname] extends Parspec<infer T> ? ParameterDefault<T> : never;
}>;

export async function env<P extends Parspecs>(parspecs: P): Promise<ReifiedParameterValues<P>> {
  return derive_values({ parspecs, defaults: {}, machines: [] });
}

export async function derive_values<P extends Parspecs>({
  parspecs,
  defaults,
  machines,
}: {
  parspecs: P;
  defaults: ParameterDefaults<P>;
  machines: VM.Any[];
}): Promise<ReifiedParameterValues<P>> {
  // FIXME: get rid of <any>?
  const result: any = {};
  const extract_from_env = (parname: string, typedesc?: string): any /* :( */ | undefined => {
    for (const key2 of env_keys(parname)) {
      const maybe_value = getenv(key2, typedesc);
      if (maybe_value !== undefined) return maybe_value;
    }
    return undefined;
  };
  for (const k in parspecs) {
    result[k] = extract_from_env(k, parspecs[k].description());
    try {
      if (result[k] === undefined && defaults[k] !== undefined) {
        result[k] = await reify_default(machines, defaults[k]);
      } else {
        const parspec = enforce_nonnull(parspecs[k]) as Parspec<any>;
        result[k] = await parspec.parser(result[k], machines);
      }
    } catch (e) {
      if (e instanceof MissingParameter) {
        throw new Error(
          `Missing parameter ${k}: please specify ${enforce_nonnull(parspecs[k]?.description())}\n`,
        );
      } else if (e instanceof ParseFailure) {
        throw new Error(
          `Unable to parse ${k}=${result[k]}: please specify ${enforce_nonnull(
            parspecs[k]?.description(),
          )}\n`,
        );
      }
      throw e;
    }
  }
  return result;
}
