<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import type { Editor } from "@tiptap/vue-3";
import { apiGet, apiPatch, apiPost, apiDelete } from "@/api/client";
import { useToastStore } from "@/stores/toast";
import { useAutoSave } from "@/composables/useAutoSave";
import { useTagSuggestions } from "@/composables/useTagSuggestions";
import { useNotesStore } from "@/stores/notes";
import TiptapEditor from "@/components/TiptapEditor.vue";
import TagInput from "@/components/TagInput.vue";
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
import WordCount from "@/components/WordCount.vue";
import { Trash2, X } from "lucide-vue-next";
import { relativeTimeOrDate } from "@/composables/useRelativeTime";

const props = defineProps<{
  projectId: number;
  activeNoteId: number | null;
}>();

const emit = defineEmits<{ "note-loaded": [id: number] }>();

interface NoteItem {
  id: number;
  title: string;
  tags: string[];
  updated_at: string;
}

const toast = useToastStore();
const notesStore = useNotesStore();

// List state
const projectNotes = ref<NoteItem[]>([]);
const listLoading = ref(false);
const deletingId = ref<number | null>(null);
const pendingDelete = ref<number | null>(null);
const searchQuery = ref("");

const filteredNotes = computed(() => {
  const q = searchQuery.value.trim().toLowerCase();
  if (!q) return projectNotes.value;
  return projectNotes.value.filter(
    (n) => n.title.toLowerCase().includes(q) || n.tags?.some((t) => t.toLowerCase().includes(q))
  );
});

function matchedTags(note: NoteItem): string[] {
  const q = searchQuery.value.trim().toLowerCase();
  if (!q) return [];
  return (note.tags ?? []).filter(
    (t) => t.toLowerCase().includes(q) && !note.title.toLowerCase().includes(q)
  );
}

// Editor state
const editingId = ref<number | null>(null);
const noteTitle = ref("");
const noteBody = ref("");
const noteTags = ref<string[]>([]);
const dirty = ref(false);
const saving = ref(false);

const tagSuggestions = useTagSuggestions(noteTitle, noteBody, noteTags, () => { dirty.value = true; });

// New note inline creation
const showNewNoteInput = ref(false);
const newNoteTitle = ref("");
const creatingNote = ref(false);
const newNoteTitleRef = ref<HTMLInputElement | null>(null);

function _noteKey(pid: number) { return `workspace_note_${pid}`; }

async function startNewNote() {
  showNewNoteInput.value = true;
  newNoteTitle.value = "";
  await nextTick();
  newNoteTitleRef.value?.focus();
}

function cancelNewNote() {
  showNewNoteInput.value = false;
  newNoteTitle.value = "";
}

async function createNote() {
  const title = newNoteTitle.value.trim();
  if (!title || creatingNote.value) return;
  creatingNote.value = true;
  try {
    const note = await apiPost<{ id: number; title: string; body: string; tags: string[]; updated_at: string }>(
      "/api/notes",
      { title, project_id: props.projectId, body: "", is_task: false }
    );
    showNewNoteInput.value = false;
    newNoteTitle.value = "";
    await loadProjectNotes();
    await openNote(note.id);
  } catch {
    toast.show("Failed to create note", "error");
  } finally {
    creatingNote.value = false;
  }
}

// TipTap editor ref
const titleRef = ref<HTMLInputElement | null>(null);
const editorRef = ref<InstanceType<typeof TiptapEditor> | null>(null);
const tiptapEditor = computed<Editor | null>(() =>
  (editorRef.value?.editor as Editor | undefined) ?? null
);

// Link suggestions
interface LinkSuggestion { note_id: number; title: string; count: number; }
const linkSuggestions = ref<LinkSuggestion[]>([]);
let linkCheckTimer: ReturnType<typeof setTimeout> | null = null;

async function fetchLinkSuggestions() {
  if (!editingId.value || !noteBody.value) { linkSuggestions.value = []; return; }
  try {
    const res = await apiPost<{ suggestions: LinkSuggestion[] }>('/api/notes/link-suggestions', {
      body: noteBody.value,
      project_id: props.projectId,
      exclude_note_id: editingId.value,
    });
    linkSuggestions.value = res.suggestions;
  } catch {
    linkSuggestions.value = [];
  }
}

function scheduleLinkCheck() {
  if (linkCheckTimer) clearTimeout(linkCheckTimer);
  linkCheckTimer = setTimeout(fetchLinkSuggestions, 2500);
}

function applyWikilinkToText(text: string, title: string): string {
  const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const parts = text.split(/(\[\[[^\]]*\]\])/);
  const pattern = new RegExp(`\\b${escaped}\\b`, 'gi');
  return parts.map((part, i) =>
    i % 2 === 0 ? part.replace(pattern, `[[${title}]]`) : part
  ).join('');
}

function applyLink(s: LinkSuggestion) {
  noteBody.value = applyWikilinkToText(noteBody.value, s.title);
  dirty.value = true;
  linkSuggestions.value = linkSuggestions.value.filter(x => x.note_id !== s.note_id);
}

function applyAllLinks() {
  let text = noteBody.value;
  for (const s of linkSuggestions.value) text = applyWikilinkToText(text, s.title);
  noteBody.value = text;
  dirty.value = true;
  linkSuggestions.value = [];
}

async function loadProjectNotes() {
  listLoading.value = true;
  try {
    const data = await apiGet<{ notes: NoteItem[] }>(
      `/api/projects/${props.projectId}/notes?type=note&limit=200`
    );
    projectNotes.value = data.notes ?? [];
  } catch {
    toast.show("Failed to load notes", "error");
  } finally {
    listLoading.value = false;
  }
}

async function openNote(id: number) {
  try {
    const note = await apiGet<{ id: number; title: string; body: string; tags: string[]; updated_at: string }>(
      `/api/notes/${id}`
    );
    editingId.value = note.id;
    noteTitle.value = note.title;
    noteBody.value = note.body ?? "";
    noteTags.value = note.tags ?? [];
    dirty.value = false;
    tagSuggestions.dismissTagSuggestions();
    linkSuggestions.value = [];
    localStorage.setItem(_noteKey(props.projectId), String(id));
    fetchLinkSuggestions();
    emit("note-loaded", note.id);
  } catch {
    toast.show("Failed to load note", "error");
  }
}

async function saveNote() {
  if (!editingId.value) return;
  saving.value = true;
  try {
    await apiPatch(`/api/notes/${editingId.value}`, {
      title: noteTitle.value,
      body: noteBody.value,
      tags: noteTags.value,
    });
    dirty.value = false;
    const idx = projectNotes.value.findIndex((n) => n.id === editingId.value);
    if (idx !== -1) {
      projectNotes.value[idx] = {
        ...projectNotes.value[idx],
        title: noteTitle.value,
        tags: noteTags.value,
        updated_at: new Date().toISOString(),
      };
      projectNotes.value.sort((a, b) =>
        new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
      );
    }
  } catch {
    toast.show("Failed to save note", "error");
  } finally {
    saving.value = false;
  }
}

function requestDelete(id: number, e: Event) {
  e.stopPropagation();
  if (deletingId.value === id) {
    confirmDelete(id);
  } else {
    deletingId.value = id;
  }
}

function cancelDelete(e: Event) {
  e.stopPropagation();
  deletingId.value = null;
}

async function confirmDelete(id: number) {
  pendingDelete.value = id;
  try {
    await apiDelete(`/api/notes/${id}`);
    projectNotes.value = projectNotes.value.filter((n) => n.id !== id);
    if (editingId.value === id) {
      editingId.value = null;
      noteTitle.value = "";
      noteBody.value = "";
      localStorage.removeItem(_noteKey(props.projectId));
    }
  } catch {
    toast.show("Failed to delete note", "error");
  } finally {
    pendingDelete.value = null;
    deletingId.value = null;
  }
}

watch(noteTitle, () => { dirty.value = true; });
watch(noteBody, () => { dirty.value = true; if (editingId.value) scheduleLinkCheck(); });
watch(noteTags, () => { dirty.value = true; });

watch(
  () => props.activeNoteId,
  async (id) => {
    if (id) {
      await openNote(id);
      await loadProjectNotes();
    }
  }
);

useAutoSave(dirty, saving, saveNote, 60_000);

onMounted(async () => {
  await loadProjectNotes();
  const stored = localStorage.getItem(_noteKey(props.projectId));
  if (stored) {
    const id = Number(stored);
    if (projectNotes.value.find((n) => n.id === id)) {
      openNote(id).catch(() => {});
    }
  }
});

onUnmounted(() => { if (linkCheckTimer) clearTimeout(linkCheckTimer); });

defineExpose({ reload: loadProjectNotes });
</script>

<template>
  <div class="ws-note-editor">

    <!-- Left rail: always-visible note list -->
    <div class="note-rail">
      <div class="rail-header">
        <template v-if="showNewNoteInput">
          <input
            ref="newNoteTitleRef"
            v-model="newNoteTitle"
            class="new-note-input"
            placeholder="Note title..."
            :disabled="creatingNote"
            @keydown.enter="createNote"
            @keydown.escape="cancelNewNote"
          />
          <button class="btn-primary btn-inline" :disabled="creatingNote || !newNoteTitle.trim()" @click="createNote">
            {{ creatingNote ? '…' : '+' }}
          </button>
          <button class="btn-text" aria-label="Cancel new note" @click="cancelNewNote"><X :size="16" /></button>
        </template>
        <template v-else>
          <span class="rail-title">Notes</span>
          <button class="btn-ghost btn-inline" @click="startNewNote" title="New note">+ New</button>
        </template>
      </div>

      <div class="rail-search">
        <input
          v-model="searchQuery"
          class="rail-search-input"
          placeholder="Search…"
          type="search"
          aria-label="Search notes"
        />
        <button v-if="searchQuery" class="btn-text btn-search-clear" aria-label="Clear search" @click="searchQuery = ''"><X :size="16" /></button>
      </div>

      <div v-if="listLoading" class="rail-state">Loading…</div>
      <ul v-else class="note-list">
        <li
          v-for="note in filteredNotes"
          :key="note.id"
          :class="['note-row', { active: editingId === note.id }]"
          @click="openNote(note.id)"
        >
          <div class="note-row-main">
            <span class="note-row-title">{{ note.title || 'Untitled' }}</span>
            <span class="note-row-age">{{ relativeTimeOrDate(note.updated_at) }}</span>
          </div>
          <div v-if="note.tags?.length" class="note-row-tags">
            <span
              v-for="tag in note.tags.slice(0, 2)"
              :key="tag"
              :class="['note-tag-pill', { 'tag-match': matchedTags(note).includes(tag) }]"
            >#{{ tag }}</span>
            <span v-if="note.tags.length > 2" class="note-tag-more">+{{ note.tags.length - 2 }}</span>
          </div>
          <div class="note-row-actions" @click.stop>
            <template v-if="deletingId === note.id">
              <button class="btn-danger-outline btn-inline" :disabled="pendingDelete === note.id" @click="requestDelete(note.id, $event)">
                {{ pendingDelete === note.id ? '…' : 'Delete?' }}
              </button>
              <button class="btn-text" aria-label="Cancel delete" @click="cancelDelete($event)"><X :size="16" /></button>
            </template>
            <button v-else class="btn-text" title="Delete note" @click="requestDelete(note.id, $event)">
              <Trash2 :size="16" />
            </button>
          </div>
        </li>
        <li v-if="filteredNotes.length === 0" class="rail-state">
          {{ searchQuery.trim() ? 'No matches.' : 'No notes yet.' }}
        </li>
      </ul>
    </div>

    <!-- Right pane: editor -->
    <div class="note-editor-pane">
      <template v-if="editingId">
        <div class="panel-header">
          <div class="editor-header-right">
            <WordCount :body="noteBody" />
            <span v-if="dirty && !saving" class="unsaved">Unsaved</span>
            <span v-if="saving" class="saving-txt">Saving…</span>
            <button class="btn-primary btn-compact" :disabled="saving || !dirty" @click="saveNote">Save</button>
          </div>
        </div>

        <div class="note-title-row">
          <input
            ref="titleRef"
            v-model="noteTitle"
            class="note-title-input"
            placeholder="Note title"
            @keydown.ctrl.s.prevent="saveNote"
            @keydown.ctrl.e.prevent="editorRef?.editor?.commands.focus()"
          />
        </div>

        <div class="tag-row">
          <TagInput v-model="noteTags" :fetchTags="(q: string) => notesStore.fetchAllTags(q)" />
          <button
            class="btn-ghost btn-compact"
            :disabled="tagSuggestions.suggestingTags.value"
            title="Auto-suggest tags from title and body"
            @click="tagSuggestions.fetchTagSuggestions()"
          >
            {{ tagSuggestions.suggestingTags.value ? '…' : 'Suggest' }}
          </button>
        </div>

        <div v-if="tagSuggestions.suggestedTags.value.length" class="tag-suggestions">
          <span class="tag-suggestions-label">Suggested:</span>
          <button
            v-for="tag in tagSuggestions.suggestedTags.value"
            :key="tag"
            :class="['btn-tag-suggestion', { applied: tagSuggestions.appliedTags.value.has(tag) }]"
            @click="tagSuggestions.applyTagSuggestion(tag)"
          >#{{ tag }}{{ tagSuggestions.appliedTags.value.has(tag) ? ' ✓' : '' }}</button>
          <button class="btn-text" aria-label="Dismiss tag suggestions" @click="tagSuggestions.dismissTagSuggestions()"><X :size="16" /></button>
        </div>

        <div class="toolbar-row">
          <MarkdownToolbar :editor="tiptapEditor" />
        </div>

        <div v-if="linkSuggestions.length" class="link-suggest-strip">
          <span class="link-suggest-label">Links:</span>
          <span v-for="s in linkSuggestions" :key="s.note_id" class="link-suggest-chip" :title="`Appears ${s.count}× unlinked`">
            <button class="btn-chip-link" @click="applyLink(s)">[[{{ s.title }}]]</button>
          </span>
          <button class="btn-ghost btn-inline" @click="applyAllLinks" title="Link all suggestions">All</button>
          <button class="btn-text" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"><X :size="16" /></button>
        </div>

        <div class="editor-area" @keydown.ctrl.s.prevent="saveNote" @keydown.ctrl.e.prevent="editorRef?.editor?.commands.focus()">
          <TiptapEditor ref="editorRef" v-model="noteBody" @escape="titleRef?.focus()" />
        </div>
      </template>

      <div v-else class="editor-empty-state">
        <p>Select a note or create a new one</p>
      </div>
    </div>

  </div>
</template>

<style scoped>
.ws-note-editor {
  display: flex;
  flex-direction: row;
  height: 100%;
  overflow: hidden;
  background: var(--fs-surface-hover);
  border-left: 1px solid var(--fs-border-color);
}

/* ── Left rail ── */
.note-rail {
  width: 200px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--fs-surface-raised);
}

.rail-header {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--fs-border-color);
  flex-shrink: 0;
}

.rail-title {
  color: var(--fs-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.72rem;
  font-weight: 500;
  flex: 1;
}


.rail-search-input {
  flex: 1;
  background: transparent;
  border: none;
  font-size: 0.78rem;
  color: var(--fs-text-primary);
  min-width: 0;
  padding: 0;
}
.rail-search-input:focus { outline: none; }
.rail-search-input::-webkit-search-cancel-button { display: none; }

/* Sits inside the search field: no padding, and must not flex. */
.btn-search-clear { padding: 0; flex-shrink: 0; }

.rail-state {
  padding: 1rem 0.65rem;
  font-size: 0.78rem;
  color: var(--fs-text-tertiary);
}

/* Note list */
.note-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1;
}

.note-row {
  display: flex;
  flex-direction: column;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid color-mix(in srgb, var(--fs-border-color) 60%, transparent);
  cursor: pointer;
  gap: 0.15rem;
  border-right: 2px solid transparent;
  transition: background 0.12s;
}
.note-row:hover { background: color-mix(in srgb, var(--fs-accent) 5%, var(--fs-surface-hover)); }
.note-row.active {
  background: color-mix(in srgb, var(--fs-accent) 8%, var(--fs-surface-hover));
  border-right-color: var(--fs-accent);
}

.note-row-main {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
}

.note-row-title {
  flex: 1;
  font-size: 0.78rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fs-text-primary);
}

.note-row-age {
  font-size: 0.62rem;
  color: var(--fs-text-tertiary);
  white-space: nowrap;
  flex-shrink: 0;
}

.note-row-tags {
  display: flex;
  gap: 0.15rem;
  overflow: hidden;
}

.note-tag-pill {
  font-size: 0.58rem;
  color: var(--fs-accent);
  background: color-mix(in srgb, var(--fs-accent) 10%, transparent);
  border-radius: 999px;
  padding: 0 0.3rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 5rem;
}
.note-tag-pill.tag-match {
  background: color-mix(in srgb, var(--fs-accent) 22%, transparent);
  outline: 1px solid var(--fs-accent);
}

.note-tag-more {
  font-size: 0.58rem;
  color: var(--fs-text-tertiary);
  white-space: nowrap;
}

.note-row-actions {
  display: flex;
  gap: 0.2rem;
  align-items: center;
}

.note-row:hover .btn-delete { opacity: 1; }

/* Editor UI */
.panel-header {
  display: flex;
  align-items: center;
  padding: 0.5rem 0.75rem;
  flex-shrink: 0;
  gap: 0.5rem;
}

.editor-header-right {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

.unsaved { font-size: 0.72rem; color: var(--fs-text-tertiary); }
.saving-txt { font-size: 0.72rem; color: var(--fs-accent); }

/* Moss action-primary per Hybrid */

.note-title-input {
  width: 100%;
  background: transparent;
  border: none;
  font-size: 1.4rem;
  font-weight: 500;
  line-height: 1.25;
  color: var(--fs-text-primary);
  padding: 0;
  font-family: 'Fraunces', serif;
  letter-spacing: -0.01em;
}
.note-title-input:focus { outline: none; }
.note-title-input::placeholder {
  color: var(--fs-text-tertiary);
}

.tag-row {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  padding: 0.4rem 0.6rem;
  flex-shrink: 0;
}
.tag-row > :first-child { flex: 1; min-width: 0; }

.btn-suggest-tags { flex-shrink: 0; align-self: center; }

.tag-suggestions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.6rem;
  background: color-mix(in srgb, var(--fs-accent) 5%, var(--fs-surface-hover));
  flex-shrink: 0;
}
.tag-suggestions-label { font-size: 0.72rem; color: var(--fs-text-tertiary); flex-shrink: 0; }

.btn-tag-suggestion {
  background: none;
  border: 1px solid var(--fs-border-color);
  border-radius: 999px;
  padding: 0.15rem 0.55rem;
  font-size: 0.75rem;
  color: var(--fs-text-primary);
  cursor: pointer;
}
.btn-tag-suggestion:hover { border-color: var(--fs-accent); color: var(--fs-accent); }
.btn-tag-suggestion.applied {
  background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
  border-color: var(--fs-accent);
  color: var(--fs-accent);
}


.link-suggest-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem;
  padding: 0.3rem 0.6rem;
  background: color-mix(in srgb, var(--fs-accent) 5%, var(--fs-surface-hover));
  flex-shrink: 0;
}
.link-suggest-label {
  font-size: 0.7rem;
  color: var(--fs-text-tertiary);
  flex-shrink: 0;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.link-suggest-chip { display: inline-flex; }

.btn-chip-link {
  background: none;
  border: 1px solid var(--fs-accent);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  font-size: 0.7rem;
  color: var(--fs-accent);
  cursor: pointer;
  font-family: monospace;
  white-space: nowrap;
}
.btn-chip-link:hover { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); }

</style>
