<script setup lang="ts">
import { ref, computed, onMounted, nextTick, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
  getSnippet,
  createSnippet,
  updateSnippet,
  type SnippetInput,
  type SnippetLocation,
} from "@/api/snippets";
import { ApiError } from "@/api/client";
import ProjectSelector from "@/components/ProjectSelector.vue";
import { useSystemsStore } from "@/stores/systems";
import { useToastStore } from "@/stores/toast";

const route = useRoute();
const router = useRouter();
const toast = useToastStore();
const systemsStore = useSystemsStore();

const editId = computed(() => (route.params.id ? Number(route.params.id) : null));
const isEditing = computed(() => editId.value !== null);

// All-string scalar state (tags/locations handled separately) so v-model and
// .trim() never meet an undefined.
interface FormState {
  name: string;
  code: string;
  language: string;
  signature: string;
  when_to_use: string;
}

const blankLocation = (): SnippetLocation => ({ repo: "", path: "", symbol: "" });

const form = ref<FormState>({
  name: "",
  code: "",
  language: "",
  signature: "",
  when_to_use: "",
});
// A snippet that unified several one-offs carries several locations; a fresh one
// starts with a single blank row.
const locations = ref<SnippetLocation[]>([blankLocation()]);

function addLocation() {
  locations.value.push(blankLocation());
}
function removeLocation(i: number) {
  locations.value.splice(i, 1);
}
const tagsText = ref("");
const projectId = ref<number | null>(null);
const systemIds = ref<number[]>([]);
const loading = ref(false);
const saving = ref(false);
const loadError = ref<string | null>(null);
const nameRef = ref<HTMLInputElement | null>(null);

// Systems belong to a project, so the picker only has anything to offer once
// one is chosen — and it repopulates when the project changes.
const projectSystems = computed(() =>
  projectId.value ? (systemsStore.systemsByProject[projectId.value] ?? []) : [],
);

async function loadSystems() {
  if (!projectId.value) return;
  try {
    await systemsStore.fetchSystems(projectId.value);
  } catch {
    /* non-fatal — the systems picker just won't populate */
  }
}

watch(projectId, (next, prev) => {
  // Moving to another project invalidates system ids from the old one.
  if (prev !== null && next !== prev) systemIds.value = [];
  loadSystems();
});

const canSave = computed(
  () => !!form.value.name.trim() && !!form.value.code.trim() && !saving.value,
);

// A near-duplicate blocked on create: the same reusable thing is already
// recorded. Recording it twice is what merge then has to undo, so the editor
// offers the existing record before it offers to save anyway.
interface DuplicateHit {
  existing_id: number;
  existing_title: string;
}
const duplicate = ref<DuplicateHit | null>(null);
const forceCreate = ref(false);

function duplicateFrom(err: unknown): DuplicateHit | null {
  if (!(err instanceof ApiError) || err.status !== 409) return null;
  const body = err.body as Record<string, unknown>;
  if (!body.duplicate) return null;
  return {
    existing_id: Number(body.existing_id),
    existing_title: String(body.existing_title ?? "an existing snippet"),
  };
}

async function load() {
  if (!isEditing.value) {
    // Recording from a project page carries the project through, so the snippet
    // lands where the work is instead of unfiled.
    if (route.query.projectId) {
      projectId.value = Number(route.query.projectId);
      await loadSystems();
    }
    await nextTick();
    nameRef.value?.focus();
    return;
  }
  loading.value = true;
  loadError.value = null;
  try {
    const s = await getSnippet(editId.value!);
    const f = s.snippet;
    form.value = {
      name: f.name,
      code: f.code,
      language: f.language,
      signature: f.signature,
      when_to_use: f.when_to_use,
    };
    locations.value = f.locations?.length
      ? f.locations.map((l) => ({ ...l }))
      : [blankLocation()];
    // The stored tag list carries language + "snippet" markers, which the
    // backend re-derives on save — show only the caller's extra tags here.
    tagsText.value = s.tags
      .filter((t) => t !== "snippet" && t !== f.language)
      .join(", ");
    projectId.value = s.project_id ?? null;
    systemIds.value = (s.systems ?? []).map((sys) => sys.id);
    await loadSystems();
  } catch {
    loadError.value = "Couldn't load this snippet to edit.";
  } finally {
    loading.value = false;
  }
}

onMounted(load);

function parseTags(): string[] {
  return tagsText.value
    .split(",")
    .map((t) => t.trim())
    .filter(Boolean);
}

function cleanLocations(): SnippetLocation[] {
  return locations.value
    .map((l) => ({ repo: l.repo.trim(), path: l.path.trim(), symbol: l.symbol.trim() }))
    .filter((l) => l.repo || l.path || l.symbol);
}

async function save() {
  if (!canSave.value) return;
  saving.value = true;
  const payload: SnippetInput = {
    name: form.value.name.trim(),
    code: form.value.code,
    language: form.value.language.trim(),
    signature: form.value.signature.trim(),
    when_to_use: form.value.when_to_use.trim(),
    locations: cleanLocations(),
    tags: parseTags(),
    project_id: projectId.value,
    system_ids: systemIds.value,
  };
  if (forceCreate.value) payload.force = true;
  try {
    const result = isEditing.value
      ? await updateSnippet(editId.value!, payload)
      : await createSnippet(payload);
    toast.show(isEditing.value ? "Snippet saved" : "Snippet created");
    router.push(`/snippets/${result.id}`);
  } catch (err) {
    // A blocked near-duplicate isn't a failure — it's the record telling you
    // this already exists. Offer the existing one rather than a red toast.
    const dup = duplicateFrom(err);
    if (dup) {
      duplicate.value = dup;
      saving.value = false;
      return;
    }
    toast.show("Failed to save snippet", "error");
  } finally {
    saving.value = false;
  }
}

async function saveAnyway() {
  duplicate.value = null;
  forceCreate.value = true;
  await save();
}

function cancel() {
  if (isEditing.value) router.push(`/snippets/${editId.value}`);
  else router.push("/snippets");
}
</script>

<template>
  <main class="snippet-editor" @keydown.ctrl.s.prevent="save" @keydown.meta.s.prevent="save">
    <router-link :to="isEditing ? `/snippets/${editId}` : '/snippets'" class="back-link">
      ← {{ isEditing ? "Back to snippet" : "Snippets" }}
    </router-link>

    <h1>{{ isEditing ? "Edit snippet" : "New snippet" }}</h1>

    <div v-if="loading" class="state-msg">Loading…</div>
    <p v-else-if="loadError" class="error-msg">{{ loadError }}</p>

    <form v-else class="form" @submit.prevent="save">
      <div class="field">
        <label for="sn-name">Name <span class="required">*</span></label>
        <input
          id="sn-name"
          ref="nameRef"
          v-model="form.name"
          type="text"
          class="input mono"
          placeholder="useDebouncedRef"
          @keydown.escape="cancel"
        />
      </div>

      <div class="field">
        <label for="sn-when">When to reach for it</label>
        <input
          id="sn-when"
          v-model="form.when_to_use"
          type="text"
          class="input"
          placeholder="Debounce a reactive ref that updates too often"
          @keydown.escape="cancel"
        />
        <p class="hint">Shown in the recall menu — keep it sharp.</p>
      </div>

      <div class="field-row">
        <div class="field">
          <label for="sn-lang">Language</label>
          <input
            id="sn-lang"
            v-model="form.language"
            type="text"
            class="input"
            placeholder="typescript"
            @keydown.escape="cancel"
          />
        </div>
        <div class="field">
          <label for="sn-sig">Signature</label>
          <input
            id="sn-sig"
            v-model="form.signature"
            type="text"
            class="input mono"
            placeholder="useDebouncedRef(value, ms)"
            @keydown.escape="cancel"
          />
        </div>
      </div>

      <fieldset class="location-set">
        <legend>
          Locations
          <span class="hint-inline">— where the reference implementation(s) live; a merged snippet keeps every call site</span>
        </legend>
        <div v-for="(loc, i) in locations" :key="i" class="loc-row">
          <input v-model="loc.repo" type="text" class="input mono" placeholder="repo" aria-label="Repo" @keydown.escape="cancel" />
          <input v-model="loc.path" type="text" class="input mono" placeholder="path" aria-label="Path" @keydown.escape="cancel" />
          <input v-model="loc.symbol" type="text" class="input mono" placeholder="symbol" aria-label="Symbol" @keydown.escape="cancel" />
          <button
            type="button"
            class="loc-remove"
            aria-label="Remove location"
            title="Remove location"
            @click="removeLocation(i)"
          >×</button>
        </div>
        <button type="button" class="loc-add" @click="addLocation">+ Add location</button>
      </fieldset>

      <div class="field">
        <label for="sn-code">Code <span class="required">*</span></label>
        <textarea
          id="sn-code"
          v-model="form.code"
          class="input mono code-area"
          rows="14"
          spellcheck="false"
          placeholder="Paste the reusable implementation…"
        ></textarea>
      </div>

      <div class="field">
        <label for="sn-tags">Tags</label>
        <input
          id="sn-tags"
          v-model="tagsText"
          type="text"
          class="input"
          placeholder="composable, ui (comma-separated)"
          @keydown.escape="cancel"
        />
        <p class="hint">Extra tags. Language and “snippet” are added automatically.</p>
      </div>

      <div class="field">
        <label for="sn-project">Project</label>
        <ProjectSelector id="sn-project" v-model="projectId" />
        <p class="hint">
          Snippets surface proactively within their project; search reaches across all of them.
        </p>
      </div>

      <div v-if="projectId" class="field">
        <span class="field-label">Systems</span>
        <div v-if="projectSystems.length" class="systems">
          <label v-for="s in projectSystems" :key="s.id" class="system-opt">
            <input type="checkbox" :value="s.id" v-model="systemIds" />
            <span>{{ s.name }}</span>
          </label>
        </div>
        <p v-else class="hint">No systems in this project yet.</p>
      </div>

      <div v-if="duplicate" class="duplicate" role="alert">
        <p class="duplicate-title">This may already be recorded</p>
        <p class="duplicate-body">
          A similar snippet exists —
          <router-link :to="`/snippets/${duplicate.existing_id}`">
            {{ duplicate.existing_title }}
          </router-link
          >. Reuse or edit that one rather than keeping two copies of the same thing.
        </p>
        <div class="duplicate-actions">
          <button type="button" class="btn-secondary" @click="saveAnyway">
            Record it anyway
          </button>
        </div>
      </div>

      <div class="actions">
        <button type="button" class="btn-secondary" @click="cancel">Cancel</button>
        <button type="submit" class="btn-primary" :disabled="!canSave">
          {{ saving ? "Saving…" : isEditing ? "Save changes" : "Create snippet" }}
        </button>
      </div>
    </form>
  </main>
</template>

<style scoped>
.snippet-editor {
  max-width: 760px;
  margin: 2rem auto;
  padding: 0 var(--page-padding-x);
  overflow-x: clip;
}
.snippet-editor h1 {
  margin: 0 0 1.25rem;
}

.back-link {
  display: inline-block;
  margin-bottom: 1rem;
  font-size: 0.85rem;
  color: var(--color-text-secondary);
  text-decoration: none;
}
.back-link:hover {
  color: var(--color-primary);
}

.state-msg {
  color: var(--color-text-muted);
  font-size: 0.9rem;
}
.error-msg {
  color: var(--color-danger);
  font-size: 0.9rem;
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}
.field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}
.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
.field-row.three {
  grid-template-columns: 1fr 1.4fr 1fr;
}
.field label,
.location-set legend {
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-text);
}
.required {
  color: var(--color-danger);
}
.hint {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin: 0.1rem 0 0;
}
.hint-inline {
  font-weight: 400;
  color: var(--color-text-muted);
}

.input {
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 0.9rem;
  font-family: inherit;
  box-sizing: border-box;
  width: 100%;
}
.input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}
.mono {
  font-family: var(--font-mono);
}
.code-area {
  resize: vertical;
  line-height: 1.6;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  tab-size: 2;
}

.location-set {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0.85rem 1rem 1rem;
  margin: 0;
}
.location-set legend {
  padding: 0 0.4rem;
}
.loc-row {
  display: grid;
  grid-template-columns: 1fr 1.4fr 1fr auto;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.5rem;
}
.loc-remove {
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
}
.loc-remove:hover {
  border-color: var(--color-danger);
  color: var(--color-danger);
}
.loc-add {
  margin-top: 0.15rem;
  padding: 0.35rem 0.7rem;
  border: 1px dashed var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.82rem;
  font-family: inherit;
}
.loc-add:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

@media (max-width: 600px) {
  .loc-row {
    grid-template-columns: 1fr 1fr;
  }
}

.field-label {
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-text);
}
.systems {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  max-height: 160px;
  overflow-y: auto;
}
.system-opt {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.85rem;
  color: var(--color-text);
  cursor: pointer;
}
.system-opt input {
  accent-color: var(--color-primary);
  cursor: pointer;
}

.duplicate {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-warning);
  border-radius: 8px;
  background: var(--color-bg-secondary);
}
.duplicate-title {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--color-text);
}
.duplicate-body {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.duplicate-body a {
  color: var(--color-primary);
}
.duplicate-actions {
  display: flex;
  justify-content: flex-end;
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.25rem;
}

@media (max-width: 600px) {
  .field-row,
  .field-row.three {
    grid-template-columns: 1fr;
  }
}
</style>
