import * as p from '@clack/prompts';
import { WizardCancelledError } from '../errors.js';
import type {
  WizardPrompter,
  SelectOption,
  MultiSelectOption,
  ProgressHandle,
} from './interface.js';

function guardCancel<T>(value: T | symbol): T {
  if (p.isCancel(value)) {
    throw new WizardCancelledError();
  }
  return value as T;
}

export class ClackPrompter implements WizardPrompter {
  intro(message: string): void {
    p.intro(message);
  }

  outro(message: string): void {
    p.outro(message);
  }

  note(message: string, title?: string): void {
    p.note(message, title);
  }

  log(message: string): void {
    p.log.info(message);
  }

  warn(message: string): void {
    p.log.warn(message);
  }

  async text(opts: {
    message: string;
    placeholder?: string;
    defaultValue?: string;
    initialValue?: string;
    validate?: (value: string) => string | void;
  }): Promise<string> {
    const validate = opts.validate
      ? (v: string) => {
          const r = opts.validate!(v);
          return r === undefined ? undefined : r;
        }
      : undefined;
    const result = await p.text({
      message: opts.message,
      placeholder: opts.placeholder,
      defaultValue: opts.defaultValue,
      initialValue: opts.initialValue,
      validate,
    });
    return guardCancel(result);
  }

  async confirm(opts: { message: string; initialValue?: boolean }): Promise<boolean> {
    const result = await p.confirm({
      message: opts.message,
      initialValue: opts.initialValue,
    });
    return guardCancel(result);
  }

  async select<T>(opts: {
    message: string;
    options: SelectOption<T>[];
    initialValue?: T;
  }): Promise<T> {
    const clackOptions = opts.options.map((o) => ({
      value: o.value as T,
      label: o.label,
      hint: o.hint,
    }));
    const result = await p.select({
      message: opts.message,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any -- clack Option conditional type needs concrete Primitive
      options: clackOptions as any,
      initialValue: opts.initialValue,
    });
    return guardCancel(result) as T;
  }

  async multiselect<T>(opts: {
    message: string;
    options: MultiSelectOption<T>[];
    required?: boolean;
  }): Promise<T[]> {
    const clackOptions = opts.options.map((o) => ({
      value: o.value as T,
      label: o.label,
      hint: o.hint,
    }));
    const result = await p.multiselect({
      message: opts.message,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      options: clackOptions as any,
      required: opts.required,
      initialValues: opts.options.filter((o) => o.selected).map((o) => o.value),
    });
    return guardCancel(result) as T[];
  }

  async groupMultiselect<T>(opts: {
    message: string;
    options: Record<string, MultiSelectOption<T>[]>;
    required?: boolean;
  }): Promise<T[]> {
    const grouped: Record<string, { value: T; label: string; hint?: string }[]> = {};
    for (const [group, items] of Object.entries(opts.options)) {
      grouped[group] = items.map((o) => ({
        value: o.value as T,
        label: o.label,
        hint: o.hint,
      }));
    }
    const result = await p.groupMultiselect({
      message: opts.message,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      options: grouped as any,
      required: opts.required,
    });
    return guardCancel(result) as T[];
  }

  spinner(): ProgressHandle {
    const s = p.spinner();
    let started = false;
    return {
      update(message: string) {
        if (!started) {
          s.start(message);
          started = true;
        } else {
          s.message(message);
        }
      },
      stop(message?: string) {
        if (started) {
          s.stop(message);
          started = false;
        }
      },
    };
  }

  separator(): void {
    p.log.info('');
  }
}
