<script setup lang="ts">
/**
 * Props-driven git code editor composition. Wraps a textarea editor
 * with a branch switcher, file-path input, and a save button. The
 * composition makes no HTTP calls — the consumer supplies Promise-
 * returning callbacks for loading branches, loading a file, and
 * (optionally) saving it.
 *
 *   <GitCodeEditor
 *     repository="gramercy/plugins"
 *     v-model:branch="branch"
 *     v-model:path="path"
 *     v-model="content"
 *     :on-load-branches="loadBranches"
 *     :on-load-file="loadFile"
 *     :on-save-file="saveFile"
 *   />
 */
import { computed, onMounted, ref, watch } from "vue"
import { GitBranch, Loader2, Save } from "@lucide/vue"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { FieldFrame } from "@/components/ui/field-frame"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"

export interface GitCodeEditorLoadedFile {
  content: string
  language?: string
}

export interface GitCodeEditorProps {
  /** Repository identifier (display only). */
  repository?: string
  /** Current branch (v-modelable). */
  branch?: string
  /** Current file path (v-modelable). */
  path?: string
  /** File content (v-modelable via modelValue). */
  modelValue?: string
  /** Lists the branches for the branch selector. */
  onLoadBranches?: () => Promise<string[]>
  /** REQUIRED loader for the current (path, branch). */
  onLoadFile?: (
    path: string,
    branch: string,
  ) => Promise<GitCodeEditorLoadedFile>
  /** Optional save callback. When omitted, the save button is hidden. */
  onSaveFile?: (path: string, branch: string, content: string) => Promise<void>
  /** Syntax hint. Default plaintext; overridden by `onLoadFile`'s response. */
  language?: string
  readonly?: boolean
  disabled?: boolean
  required?: boolean
  label?: string
  hideLabel?: boolean
  name?: string | null
  id?: string
  helpText?: string
  errors?: string[]
  classList?: string[] | string
}

const props = withDefaults(defineProps<GitCodeEditorProps>(), {
  errors: () => [],
  language: "plaintext",
  label: "Git Code Editor",
})

const emit = defineEmits<{
  "update:modelValue": [value: string]
  "update:branch": [value: string]
  "update:path": [value: string]
  "update:language": [value: string]
}>()

const content = ref<string>(props.modelValue ?? "")
const branchRef = ref<string>(props.branch ?? "")
const pathRef = ref<string>(props.path ?? "")
const languageRef = ref<string>(props.language ?? "plaintext")

watch(
  () => props.modelValue,
  (next) => {
    if (next !== undefined && next !== content.value) {
      content.value = next
    }
  },
)
watch(
  () => props.branch,
  (next) => {
    if (next !== undefined && next !== branchRef.value) {
      branchRef.value = next
    }
  },
)
watch(
  () => props.path,
  (next) => {
    if (next !== undefined && next !== pathRef.value) {
      pathRef.value = next
    }
  },
)
watch(
  () => props.language,
  (next) => {
    if (next !== undefined && next !== languageRef.value) {
      languageRef.value = next
    }
  },
)

function updateContent(next: string | undefined): void {
  const value = next ?? ""
  content.value = value
  emit("update:modelValue", value)
}

function updateBranch(next: string): void {
  branchRef.value = next
  emit("update:branch", next)
}

function updatePath(next: string | number | undefined): void {
  const v = next === undefined ? "" : String(next)
  pathRef.value = v
  emit("update:path", v)
}

// --- branches loader -------------------------------------------------
const branches = ref<string[]>([])
const branchesLoading = ref<boolean>(false)
const branchesError = ref<string | null>(null)

async function loadBranches(): Promise<void> {
  if (!props.onLoadBranches) {
    return
  }
  branchesLoading.value = true
  branchesError.value = null
  try {
    const next = await props.onLoadBranches()
    branches.value = Array.isArray(next) ? next : []
  } catch (err) {
    branchesError.value = err instanceof Error ? err.message : String(err)
    branches.value = []
  } finally {
    branchesLoading.value = false
  }
}

// --- file loader -----------------------------------------------------
const fileLoading = ref<boolean>(false)
const fileError = ref<string | null>(null)
let fileToken = 0

async function loadFile(): Promise<void> {
  if (!props.onLoadFile) {
    return
  }
  if (!pathRef.value || !branchRef.value) {
    return
  }
  const myToken = ++fileToken
  fileLoading.value = true
  fileError.value = null
  try {
    const result = await props.onLoadFile(pathRef.value, branchRef.value)
    if (myToken !== fileToken) {
      return
    }
    const next = result?.content ?? ""
    content.value = next
    emit("update:modelValue", next)
    if (result?.language) {
      languageRef.value = result.language
      emit("update:language", result.language)
    }
  } catch (err) {
    if (myToken !== fileToken) {
      return
    }
    fileError.value = err instanceof Error ? err.message : String(err)
  } finally {
    if (myToken === fileToken) {
      fileLoading.value = false
    }
  }
}

watch([branchRef, pathRef], () => {
  void loadFile()
})

// --- save ------------------------------------------------------------
const saving = ref<boolean>(false)
const saveError = ref<string | null>(null)

async function save(): Promise<void> {
  if (!props.onSaveFile) {
    return
  }
  if (!pathRef.value || !branchRef.value) {
    return
  }
  saving.value = true
  saveError.value = null
  try {
    await props.onSaveFile(pathRef.value, branchRef.value, content.value)
  } catch (err) {
    saveError.value = err instanceof Error ? err.message : String(err)
  } finally {
    saving.value = false
  }
}

const branchOptions = computed<string[]>(() => {
  const all = new Set(branches.value)
  if (branchRef.value) {
    all.add(branchRef.value)
  }
  return Array.from(all)
})

const allErrors = computed<string[]>(() => {
  const out = [...(props.errors ?? [])]
  if (fileError.value) {
    out.push(fileError.value)
  }
  if (saveError.value) {
    out.push(saveError.value)
  }
  return out
})

const showSave = computed<boolean>(() => Boolean(props.onSaveFile))

onMounted(() => {
  void loadBranches()
  void loadFile()
})

defineExpose({ reload: loadFile, save })
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="allErrors"
    :class="cn(props.classList)"
  >
    <Card
      :data-element-name="props.name ?? undefined"
      data-testid="git-code-editor"
    >
      <CardHeader class="flex flex-col gap-2 border-b py-3">
        <div class="flex flex-wrap items-center gap-2">
          <div
            v-if="props.repository"
            class="flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground"
            data-testid="git-code-editor-repository"
          >
            <GitBranch class="size-3.5" />
            <span>{{ props.repository }}</span>
          </div>

          <div class="min-w-40" data-testid="git-code-editor-branch-wrapper">
            <Select
              :model-value="branchRef"
              :disabled="disabled || readonly || branchesLoading"
              @update:model-value="(v) => updateBranch(String(v ?? ''))"
            >
              <SelectTrigger
                data-testid="git-code-editor-branch-trigger"
                :aria-label="'Branch'"
              >
                <SelectValue placeholder="Select branch" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem
                  v-for="b in branchOptions"
                  :key="b"
                  :value="b"
                  :data-testid="`git-code-editor-branch-${b}`"
                >
                  {{ b }}
                </SelectItem>
              </SelectContent>
            </Select>
          </div>

          <Input
            :model-value="pathRef"
            placeholder="path/to/file"
            class="flex-1 min-w-48"
            :disabled="disabled || readonly"
            data-testid="git-code-editor-path-input"
            @update:model-value="updatePath"
          />

          <Button
            v-if="showSave"
            type="button"
            variant="default"
            size="sm"
            :disabled="
              saving || disabled || readonly || !pathRef || !branchRef
            "
            data-testid="git-code-editor-save"
            @click="save"
          >
            <Loader2 v-if="saving" class="size-4 animate-spin" />
            <Save v-else class="size-4" />
            <span>{{ saving ? "Saving…" : "Save" }}</span>
          </Button>
        </div>
        <p
          v-if="languageRef"
          class="text-[10px] uppercase tracking-wide text-muted-foreground"
          data-testid="git-code-editor-language"
        >
          {{ languageRef }}
        </p>
      </CardHeader>
      <CardContent class="p-0">
        <div
          v-if="fileLoading"
          class="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground"
          data-testid="git-code-editor-loading"
        >
          <Loader2 class="size-4 animate-spin" />
          <span>Loading file…</span>
        </div>
        <Textarea
          :id="id"
          :model-value="content"
          :readonly="readonly || fileLoading"
          :disabled="disabled"
          :required="required"
          :rows="16"
          spellcheck="false"
          class="min-h-64 rounded-none border-0 font-mono text-xs focus-visible:ring-0 focus-visible:ring-offset-0"
          :data-element-name="props.name ?? undefined"
          data-testid="git-code-editor-textarea"
          @update:model-value="updateContent"
        />
      </CardContent>
    </Card>
  </FieldFrame>
</template>
