import { type App, Notice, Platform, SuggestModal } from "obsidian";
import { shortThreadId } from "../../domain/threads/id";
import type { Thread } from "../../domain/threads/model";
import { threadSearchMatches } from "../../domain/threads/search";
import type { ThreadCatalogActiveReader } from "../threads/catalog/thread-catalog";

export interface ThreadPickerHost {
  readonly app: App;
  readonly threadCatalog: ThreadCatalogActiveReader;
  openThreadInCurrentView(threadId: string): Promise<void>;
  openThreadInAvailableView(threadId: string): Promise<void>;
}

interface ThreadSuggestion {
  thread: Thread;
  title: string;
}

type ThreadOpenMode = "current" | "available";

const THREAD_PICKER_MODIFIER_ENTER_LISTENER_OPTIONS = { capture: true } as const;

export async function openThreadPicker(host: ThreadPickerHost): Promise<void> {
  try {
    const threads = await host.threadCatalog.refreshActive();
    if (threads.length === 0) {
      new Notice("No Codex threads found.");
      return;
    }
    new ThreadPickerModal(host, threads).open();
  } catch (error) {
    new Notice(error instanceof Error ? error.message : String(error));
  }
}

function threadPickerSuggestions(threads: readonly Thread[], queryText: string): ThreadSuggestion[] {
  return threadSearchMatches(threads, queryText).map(({ thread, title }) => ({
    thread,
    title,
  }));
}

function threadOpenModeFromEvent(evt: MouseEvent | KeyboardEvent): ThreadOpenMode {
  if (evt instanceof KeyboardEvent && (evt.metaKey || evt.ctrlKey)) return "available";
  return "current";
}

class ThreadPickerModal extends SuggestModal<ThreadSuggestion> {
  private threads: readonly Thread[];
  private completeThreadsPromise: Promise<readonly Thread[]> | null = null;
  private hasCompleteThreadList = false;

  constructor(
    private readonly host: ThreadPickerHost,
    threads: readonly Thread[],
  ) {
    super(host.app);
    this.threads = threads;
    this.limit = threads.length;
    this.emptyStateText = "No matching Codex threads";
    this.setPlaceholder("Open Codex thread...");
    this.setInstructions([
      { command: "↵", purpose: "to open in current panel" },
      { command: Platform.isMacOS ? "⌘ ↵" : "Ctrl ↵", purpose: "to open in new panel" },
    ]);
  }

  override onOpen(): void {
    void super.onOpen();
    this.inputEl.addEventListener("keydown", this.handleInputKeydown, THREAD_PICKER_MODIFIER_ENTER_LISTENER_OPTIONS);
  }

  override onClose(): void {
    this.inputEl.removeEventListener("keydown", this.handleInputKeydown, THREAD_PICKER_MODIFIER_ENTER_LISTENER_OPTIONS);
    super.onClose();
  }

  override async getSuggestions(query: string): Promise<ThreadSuggestion[]> {
    if (query.trim().length > 0) {
      try {
        await this.loadCompleteThreadList();
      } catch (error) {
        new Notice(error instanceof Error ? error.message : String(error));
      }
    }
    return threadPickerSuggestions(this.threads, query);
  }

  override renderSuggestion(value: ThreadSuggestion, el: HTMLElement): void {
    const contentEl = el.createDiv({ cls: "suggestion-content" });
    contentEl.createDiv({ cls: "suggestion-title", text: value.title });
    el.createDiv({ cls: "suggestion-note", text: shortThreadId(value.thread.id) });
  }

  override onChooseSuggestion(item: ThreadSuggestion, evt: MouseEvent | KeyboardEvent): void {
    void this.openThread(item.thread.id, threadOpenModeFromEvent(evt));
  }

  private readonly handleInputKeydown = (evt: KeyboardEvent): void => {
    if (evt.key !== "Enter" || (!evt.metaKey && !evt.ctrlKey)) return;
    evt.preventDefault();
    evt.stopImmediatePropagation();
    this.selectActiveSuggestion(evt);
  };

  private async openThread(threadId: string, mode: ThreadOpenMode): Promise<void> {
    try {
      if (mode === "available") {
        await this.host.openThreadInAvailableView(threadId);
      } else {
        await this.host.openThreadInCurrentView(threadId);
      }
    } catch (error) {
      new Notice(error instanceof Error ? error.message : String(error));
    }
  }

  private async loadCompleteThreadList(): Promise<void> {
    if (this.hasCompleteThreadList) return;
    const pending = this.completeThreadsPromise ?? this.host.threadCatalog.loadActive();
    this.completeThreadsPromise = pending;
    try {
      this.threads = await pending;
      this.limit = this.threads.length;
      this.hasCompleteThreadList = true;
    } finally {
      if (this.completeThreadsPromise === pending) this.completeThreadsPromise = null;
    }
  }
}
