import type { Theme } from "@earendil-works/pi-coding-agent";
import {
  Editor,
  type Component,
  type EditorTheme,
  type Focusable,
  Key,
  matchesKey,
  type TUI,
  visibleWidth,
  wrapTextWithAnsi,
} from "@earendil-works/pi-tui";

import { orderedAnswers } from "./normalize.ts";
import type { AskUserResult, NormalizedQuestion, QuestionOptionInput, UserAnswer } from "./types.ts";

type InputKind = "text" | "custom";
type Done = (result: AskUserResult) => void;
type RenderOption = QuestionOptionInput & { kind: "option" | "custom" | "skip" };

export class AskUserView implements Component, Focusable {
  private readonly editor: Editor;
  private readonly answers = new Map<string, UserAnswer>();
  private readonly abortHandler: () => void;
  private currentPage = 0;
  private optionIndex = 0;
  private inputKind: InputKind | null = null;
  private validationMessage: string | undefined;
  private cachedWidth: number | undefined;
  private cachedLines: string[] | undefined;
  private completed = false;
  private _focused = false;

  constructor(
    private readonly tui: TUI,
    private readonly theme: Theme,
    private readonly questions: NormalizedQuestion[],
    private readonly done: Done,
    private readonly signal?: AbortSignal,
  ) {
    const editorTheme: EditorTheme = {
      borderColor: (text) => theme.fg("accent", text),
      selectList: {
        selectedPrefix: (text) => theme.fg("accent", text),
        selectedText: (text) => theme.fg("accent", text),
        description: (text) => theme.fg("muted", text),
        scrollInfo: (text) => theme.fg("dim", text),
        noMatch: (text) => theme.fg("warning", text),
      },
    };
    this.editor = new Editor(tui, editorTheme);
    this.editor.onSubmit = (value) => this.submitEditor(value);
    this.abortHandler = () => this.finish(true);
    signal?.addEventListener("abort", this.abortHandler, { once: true });
  }

  get focused(): boolean {
    return this._focused;
  }

  set focused(value: boolean) {
    this._focused = value;
    this.syncEditorFocus();
  }

  private syncEditorFocus(): void {
    this.editor.focused = this._focused && this.inputKind !== null;
  }

  private refresh(): void {
    this.cachedWidth = undefined;
    this.cachedLines = undefined;
    this.syncEditorFocus();
    this.tui.requestRender();
  }

  private currentQuestion(): NormalizedQuestion | undefined {
    return this.questions[this.currentPage];
  }

  private currentOptions(): RenderOption[] {
    const question = this.currentQuestion();
    if (!question || question.type !== "select") return [];
    const options: RenderOption[] = question.options.map((option) => ({ ...option, kind: "option" }));
    if (question.allowCustom) options.push({ value: "", label: "Type something.", kind: "custom" });
    if (!question.required) options.push({ value: "", label: "Skip this question.", kind: "skip" });
    return options;
  }

  private allAnswered(): boolean {
    return this.questions.every((question) => this.answers.has(question.id));
  }

  private setCurrentPage(page: number): void {
    this.currentPage = page;
    this.inputKind = null;
    this.validationMessage = undefined;
    this.editor.setText("");
    this.optionIndex = 0;

    const question = this.currentQuestion();
    const answer = question ? this.answers.get(question.id) : undefined;
    if (question?.type === "select" && answer) {
      const options = this.currentOptions();
      const selected = options.findIndex((option) =>
        answer.skipped ? option.kind === "skip" : answer.custom ? option.kind === "custom" : option.kind === "option" && option.value === answer.value,
      );
      this.optionIndex = Math.max(0, selected);
    }
    this.refresh();
  }

  private movePage(delta: number): void {
    const pageCount = this.questions.length + 1;
    this.setCurrentPage((this.currentPage + delta + pageCount) % pageCount);
  }

  private enterEditor(kind: InputKind): void {
    const question = this.currentQuestion();
    if (!question) return;
    const existing = this.answers.get(question.id);
    this.inputKind = kind;
    this.validationMessage = undefined;
    this.editor.setText(existing && (question.type === "text" || existing.custom) ? existing.answer : "");
    this.refresh();
  }

  private saveAnswer(question: NormalizedQuestion, answer: Omit<UserAnswer, "id" | "question" | "type">): void {
    this.answers.set(question.id, {
      id: question.id,
      question: question.prompt,
      type: question.type,
      ...answer,
    });
  }

  private submitEditor(value: string): void {
    const question = this.currentQuestion();
    if (!question || !this.inputKind) return;
    const trimmed = value.trim();
    if (!trimmed && (question.required || this.inputKind === "custom")) {
      this.validationMessage = "An answer is required.";
      this.refresh();
      return;
    }

    this.saveAnswer(question, {
      answer: trimmed,
      value: trimmed,
      label: trimmed,
      custom: this.inputKind === "custom",
      skipped: !trimmed,
    });
    this.inputKind = null;
    this.editor.setText("");
    this.advanceAfterAnswer();
  }

  private selectCurrentOption(): void {
    const question = this.currentQuestion();
    const option = this.currentOptions()[this.optionIndex];
    if (!question || !option) return;
    if (option.kind === "custom") {
      this.enterEditor("custom");
      return;
    }
    if (option.kind === "skip") {
      this.saveAnswer(question, { answer: "", value: "", label: "Skipped", custom: false, skipped: true });
    } else {
      this.saveAnswer(question, {
        answer: option.label,
        value: option.value,
        label: option.label,
        custom: false,
        skipped: false,
      });
    }
    this.advanceAfterAnswer();
  }

  private advanceAfterAnswer(): void {
    if (this.questions.length === 1) {
      this.finish(false);
      return;
    }
    this.setCurrentPage(this.currentPage < this.questions.length - 1 ? this.currentPage + 1 : this.questions.length);
  }

  private finish(cancelled: boolean): void {
    if (this.completed) return;
    this.completed = true;
    this.signal?.removeEventListener("abort", this.abortHandler);
    this.done({
      version: 1,
      cancelled,
      questions: this.questions,
      answers: orderedAnswers(this.questions, this.answers),
    });
  }

  handleInput(data: string): void {
    if (this.inputKind) {
      if (matchesKey(data, Key.escape)) {
        this.inputKind = null;
        this.validationMessage = undefined;
        this.editor.setText("");
        this.refresh();
        return;
      }
      this.editor.handleInput(data);
      this.refresh();
      return;
    }

    if (matchesKey(data, Key.escape)) {
      this.finish(true);
      return;
    }

    if (this.questions.length > 1) {
      if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
        this.movePage(1);
        return;
      }
      if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
        this.movePage(-1);
        return;
      }
    }

    if (this.currentPage === this.questions.length) {
      if (matchesKey(data, Key.enter) && this.allAnswered()) this.finish(false);
      return;
    }

    const question = this.currentQuestion();
    if (!question) return;
    if (question.type === "text") {
      if (!question.required && matchesKey(data, "s")) {
        this.saveAnswer(question, { answer: "", value: "", label: "Skipped", custom: false, skipped: true });
        this.advanceAfterAnswer();
      } else if (matchesKey(data, Key.enter)) {
        this.enterEditor("text");
      }
      return;
    }

    const options = this.currentOptions();
    if (matchesKey(data, Key.up)) {
      this.optionIndex = Math.max(0, this.optionIndex - 1);
      this.refresh();
      return;
    }
    if (matchesKey(data, Key.down)) {
      this.optionIndex = Math.min(options.length - 1, this.optionIndex + 1);
      this.refresh();
      return;
    }
    if (matchesKey(data, Key.enter)) this.selectCurrentOption();
  }

  render(width: number): string[] {
    const renderWidth = Math.max(1, width);
    if (this.cachedLines && this.cachedWidth === renderWidth) return this.cachedLines;

    const lines: string[] = [];
    const question = this.currentQuestion();
    const addWrapped = (text: string): void => {
      lines.push(...wrapTextWithAnsi(text, renderWidth));
    };
    const addWrappedWithPrefix = (prefix: string, text: string): void => {
      const prefixWidth = visibleWidth(prefix);
      if (prefixWidth >= renderWidth) {
        addWrapped(prefix + text);
        return;
      }
      const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
      const continuation = " ".repeat(prefixWidth);
      wrapped.forEach((line, index) => lines.push(`${index === 0 ? prefix : continuation}${line}`));
    };

    lines.push(this.theme.fg("accent", "─".repeat(renderWidth)));

    if (this.questions.length > 1) {
      const tabs = this.questions.map((item, index) => {
        const answered = this.answers.has(item.id);
        const text = ` ${answered ? "■" : "□"} ${item.label} `;
        return index === this.currentPage
          ? this.theme.bg("selectedBg", this.theme.fg("text", text))
          : this.theme.fg(answered ? "success" : "muted", text);
      });
      const reviewText = " ✓ Review ";
      tabs.push(this.currentPage === this.questions.length
        ? this.theme.bg("selectedBg", this.theme.fg("text", reviewText))
        : this.theme.fg(this.allAnswered() ? "success" : "dim", reviewText));
      addWrappedWithPrefix(" ", tabs.join(" "));
      lines.push("");
    }

    if (this.currentPage === this.questions.length) {
      addWrappedWithPrefix(" ", this.theme.fg("accent", this.theme.bold("Review answers")));
      lines.push("");
      for (const item of this.questions) {
        const answer = this.answers.get(item.id);
        const value = !answer ? this.theme.fg("warning", "Unanswered") : answer.skipped ? this.theme.fg("muted", "Skipped") : this.theme.fg("text", answer.label);
        addWrappedWithPrefix(" ", `${this.theme.fg("muted", `${item.label}: `)}${value}`);
      }
      lines.push("");
      addWrappedWithPrefix(" ", this.allAnswered()
        ? this.theme.fg("success", "Press Enter to submit")
        : this.theme.fg("warning", "Answer or explicitly skip every question before submitting"));
    } else if (question) {
      addWrappedWithPrefix(" ", this.theme.fg("text", question.prompt));
      lines.push("");

      if (question.type === "select") {
        for (const [index, option] of this.currentOptions().entries()) {
          const selected = index === this.optionIndex;
          const prefix = selected ? this.theme.fg("accent", "> ") : "  ";
          addWrappedWithPrefix(prefix, this.theme.fg(selected ? "accent" : option.kind === "skip" ? "muted" : "text", `${index + 1}. ${option.label}`));
          if (option.description) addWrappedWithPrefix("     ", this.theme.fg("muted", option.description));
        }
      } else if (!this.inputKind) {
        const existing = this.answers.get(question.id);
        if (existing) addWrappedWithPrefix(" ", `${this.theme.fg("muted", "Current answer: ")}${existing.skipped ? "Skipped" : existing.label}`);
        const action = `${existing ? "Press Enter to edit" : "Press Enter to answer"}${question.required ? "" : " • s to skip"}`;
        addWrappedWithPrefix(" ", this.theme.fg("accent", action));
      }

      if (this.inputKind) {
        if (this.inputKind === "custom") lines.push("");
        addWrappedWithPrefix(" ", this.theme.fg("muted", "Your answer:"));
        for (const line of this.editor.render(Math.max(1, renderWidth - 2))) lines.push(` ${line}`);
        if (this.validationMessage) addWrappedWithPrefix(" ", this.theme.fg("warning", this.validationMessage));
      }
    }

    lines.push("");
    const help = this.inputKind
      ? "Enter submit • Esc go back"
      : this.currentPage === this.questions.length
        ? "Tab/←→ navigate • Enter submit • Esc cancel"
        : question?.type === "text"
          ? `${question.required ? "Enter write" : "Enter write • s skip"} • Tab/←→ navigate • Esc cancel`
          : "↑↓ select • Enter confirm • Tab/←→ navigate • Esc cancel";
    addWrappedWithPrefix(" ", this.theme.fg("dim", this.questions.length === 1 ? help.replace(" • Tab/←→ navigate", "") : help));
    lines.push(this.theme.fg("accent", "─".repeat(renderWidth)));

    this.cachedWidth = renderWidth;
    this.cachedLines = lines;
    return lines;
  }

  invalidate(): void {
    this.cachedWidth = undefined;
    this.cachedLines = undefined;
    this.editor.invalidate();
  }

  dispose(): void {
    this.signal?.removeEventListener("abort", this.abortHandler);
  }
}
