<script setup lang="ts">
import {
  AlertTriangle,
  CheckCircle2,
  ChevronRight,
  FileJson2,
  Loader2,
  RefreshCw,
  Save,
  Search,
  ShieldCheck,
} from '@lucide/vue'
import { computed, onMounted, ref } from 'vue'
import { apiFetch } from '../../services/api'
import { createMutationRequestContext } from '../../services/mutationContext'
import { reportOperationEnvelope } from '../../services/operationResults'

interface SchemaChild {
  key: string
  path: string
  type: unknown
  required: boolean
  hasChildren: boolean
  reloadKind: string | null
  hint: unknown
}

interface SchemaLookup {
  path: string
  schema: unknown
  reloadKind: string | null
  hint: unknown
  children: SchemaChild[]
  checkedAt: string
}

interface ConfigSnapshot {
  exists: boolean
  valid: boolean
  hash: string | null
  config: unknown
  issues: unknown
  warnings: unknown
  checkedAt: string
}

interface ConfigPatchResult {
  ok: boolean
  state: string
  message: string
  snapshot: ConfigSnapshot
  restart: unknown
  verified: boolean
  idempotencyKey: string
  correlationId: string
  completedAt: string
  operation?: unknown
}

const schemaPath = ref('agents')
const replacePathsText = ref('')
const note = ref('')
const patchText = ref('{\n  \n}')
const snapshot = ref<ConfigSnapshot | null>(null)
const schema = ref<SchemaLookup | null>(null)
const loading = ref(false)
const schemaLoading = ref(false)
const saving = ref(false)
const error = ref('')
const success = ref('')
const confirmed = ref(false)

const parsedPatch = computed<Record<string, unknown> | null>(() => {
  try {
    const value = JSON.parse(patchText.value) as unknown
    return isPlainObject(value) ? value : null
  } catch {
    return null
  }
})

const patchError = computed(() => {
  if (!parsedPatch.value) return 'Der Patch muss ein gültiges JSON-Objekt sein.'
  if (Object.keys(parsedPatch.value).length === 0) return 'Der Patch enthält keine Änderung.'
  return ''
})

const selectedBefore = computed(() =>
  readPath(snapshot.value?.config, schemaPath.value),
)
const selectedAfter = computed(() => {
  if (!snapshot.value || !parsedPatch.value) return null
  const merged = mergePatch(cloneJson(snapshot.value.config), parsedPatch.value)
  return readPath(merged, schemaPath.value)
})
const replacePaths = computed(() =>
  replacePathsText.value
    .split(',')
    .map(path => path.trim())
    .filter(Boolean),
)
const canSave = computed(() =>
  Boolean(snapshot.value?.hash)
  && !patchError.value
  && confirmed.value
  && !saving.value,
)

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function cloneJson<T>(value: T): T {
  return value == null ? value : JSON.parse(JSON.stringify(value)) as T
}

function mergePatch(target: unknown, patch: unknown): unknown {
  if (!isPlainObject(patch)) return cloneJson(patch)
  const result: Record<string, unknown> = isPlainObject(target)
    ? { ...target }
    : {}
  for (const [key, value] of Object.entries(patch)) {
    if (value === null) {
      delete result[key]
    } else {
      result[key] = mergePatch(result[key], value)
    }
  }
  return result
}

function readPath(root: unknown, path: string): unknown {
  const segments = path
    .replace(/\[(\d+)\]/g, '.$1')
    .split(/[./]/)
    .map(segment => segment.trim())
    .filter(Boolean)
  let current = root
  for (const segment of segments) {
    if (Array.isArray(current) && /^\d+$/.test(segment)) {
      current = current[Number(segment)]
      continue
    }
    if (!isPlainObject(current)) return null
    current = current[segment]
  }
  return current ?? null
}

function formatJson(value: unknown) {
  if (value == null) return 'Nicht vorhanden'
  return JSON.stringify(value, null, 2)
}

async function readResponse<T>(response: Response, operationTitle?: string): Promise<T> {
  const payload = await response.json().catch(() => null) as
    | T
    | { message?: string; detail?: string; title?: string }
    | null
  if (operationTitle) reportOperationEnvelope(payload, operationTitle)
  if (!response.ok) {
    const message = payload && typeof payload === 'object'
      ? ('message' in payload && payload.message)
        || ('detail' in payload && payload.detail)
        || ('title' in payload && payload.title)
      : null
    throw new Error(message || `OpenClaw-Anfrage ist mit HTTP ${response.status} fehlgeschlagen.`)
  }
  return payload as T
}

async function loadSnapshot() {
  loading.value = true
  error.value = ''
  success.value = ''
  try {
    snapshot.value = await readResponse<ConfigSnapshot>(
      await apiFetch('/api/v1/openclaw/config'),
    )
  } catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'Konfiguration konnte nicht geladen werden.'
  } finally {
    loading.value = false
  }
}

async function loadSchema(path = schemaPath.value) {
  const normalized = path.trim()
  if (!normalized) {
    error.value = 'Ein OpenClaw-Konfigurationspfad ist erforderlich.'
    return
  }
  schemaLoading.value = true
  error.value = ''
  success.value = ''
  try {
    schema.value = await readResponse<SchemaLookup>(
      await apiFetch(`/api/v1/openclaw/config/schema?path=${encodeURIComponent(normalized)}`),
    )
    schemaPath.value = schema.value.path
  } catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'Schema konnte nicht geladen werden.'
  } finally {
    schemaLoading.value = false
  }
}

async function savePatch() {
  if (!canSave.value || !snapshot.value?.hash || !parsedPatch.value) return
  saving.value = true
  error.value = ''
  success.value = ''
  try {
    const context = createMutationRequestContext('openclaw-config-patch')
    const result = await readResponse<ConfigPatchResult>(
      await apiFetch('/api/v1/openclaw/config', {
        method: 'PATCH',
        headers: context.headers,
        body: JSON.stringify({
          patch: parsedPatch.value,
          baseHash: snapshot.value.hash,
          replacePaths: replacePaths.value,
          note: note.value.trim() || null,
        }),
      }),
      'OpenClaw-Konfiguration aktualisiert',
    )
    snapshot.value = result.snapshot
    success.value = result.verified
      ? `${result.message} Die Konfiguration wurde zurückgelesen und verifiziert.`
      : result.message
    confirmed.value = false
    patchText.value = '{\n  \n}'
    replacePathsText.value = ''
    note.value = ''
  } catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'Konfigurationsänderung ist fehlgeschlagen.'
  } finally {
    saving.value = false
  }
}

onMounted(async () => {
  await Promise.all([loadSnapshot(), loadSchema()])
})
</script>

<template>
  <section class="config-editor nexus-panel" aria-labelledby="openclaw-config-title">
    <header class="config-editor__header">
      <div>
        <span class="config-editor__eyebrow">SCHEMA-CONTROLLED</span>
        <h2 id="openclaw-config-title">
          <FileJson2 :size="18" aria-hidden="true" />
          OpenClaw-Konfiguration
        </h2>
        <p>
          Nexus liest das Live-Schema und schreibt ausschließlich hash-geschützte Patches.
          Provider-Secrets werden weder angezeigt noch als Klartext akzeptiert.
        </p>
      </div>
      <button
        type="button"
        class="nexus-button"
        :disabled="loading || saving"
        @click="loadSnapshot"
      >
        <Loader2 v-if="loading" :size="14" class="spin" aria-hidden="true" />
        <RefreshCw v-else :size="14" aria-hidden="true" />
        Neu laden
      </button>
    </header>

    <div v-if="loading && !snapshot" class="nexus-state nexus-state--loading" role="status">
      <Loader2 :size="18" class="spin" aria-hidden="true" />
      <p>OpenClaw-Konfiguration wird geladen…</p>
    </div>

    <template v-else>
      <div class="config-editor__facts" aria-label="Konfigurationsstatus">
        <div>
          <span>Basis-Hash</span>
          <code>{{ snapshot?.hash || 'nicht verfügbar' }}</code>
        </div>
        <div>
          <span>Validierung</span>
          <strong :class="snapshot?.valid ? 'is-valid' : 'is-warning'">
            {{ snapshot?.valid ? 'gültig' : 'Prüfung erforderlich' }}
          </strong>
        </div>
        <div>
          <span>Schema-Pfad</span>
          <code>{{ schema?.path || schemaPath }}</code>
        </div>
      </div>

      <form class="config-editor__schema-search" @submit.prevent="loadSchema()">
        <label for="openclaw-schema-path">Konfigurationsbereich</label>
        <div>
          <Search :size="15" aria-hidden="true" />
          <input
            id="openclaw-schema-path"
            v-model="schemaPath"
            autocomplete="off"
            spellcheck="false"
            placeholder="z. B. agents, cron oder models"
          />
          <button type="submit" class="nexus-button" :disabled="schemaLoading">
            <Loader2 v-if="schemaLoading" :size="14" class="spin" aria-hidden="true" />
            Schema laden
          </button>
        </div>
      </form>

      <nav
        v-if="schema?.children.length"
        class="config-editor__children"
        aria-label="Untergeordnete Konfigurationsbereiche"
      >
        <button
          v-for="child in schema.children"
          :key="child.path"
          type="button"
          @click="loadSchema(child.path)"
        >
          <span>
            <strong>{{ child.key }}</strong>
            <small>{{ child.reloadKind || 'Live-Verhalten laut OpenClaw' }}</small>
          </span>
          <ChevronRight :size="14" aria-hidden="true" />
        </button>
      </nav>

      <div class="config-editor__grid">
        <section aria-labelledby="openclaw-patch-title">
          <h3 id="openclaw-patch-title">JSON Merge Patch</h3>
          <label for="openclaw-config-patch">Patch</label>
          <textarea
            id="openclaw-config-patch"
            v-model="patchText"
            rows="12"
            spellcheck="false"
            @input="confirmed = false"
          />
          <p v-if="patchError" class="config-editor__validation">{{ patchError }}</p>

          <label for="openclaw-replace-paths">Replace-Pfade <span>(optional, kommasepariert)</span></label>
          <input
            id="openclaw-replace-paths"
            v-model="replacePathsText"
            spellcheck="false"
            placeholder="agents.list, models.providers"
            @input="confirmed = false"
          />

          <label for="openclaw-config-note">Audit-Notiz <span>(optional)</span></label>
          <input
            id="openclaw-config-note"
            v-model="note"
            maxlength="1000"
            placeholder="Warum wird diese Änderung benötigt?"
          />
        </section>

        <section class="config-editor__diff" aria-labelledby="openclaw-diff-title">
          <h3 id="openclaw-diff-title">Diff-Vorschau · {{ schemaPath }}</h3>
          <div>
            <span>Vorher</span>
            <pre>{{ formatJson(selectedBefore) }}</pre>
          </div>
          <div>
            <span>Nach Patch</span>
            <pre>{{ formatJson(selectedAfter) }}</pre>
          </div>
        </section>
      </div>

      <div class="config-editor__guard">
        <AlertTriangle :size="18" aria-hidden="true" />
        <div>
          <strong>OpenClaw bleibt die Konfigurationsautorität.</strong>
          <p>
            Bei einem abweichenden Basis-Hash bricht Nexus mit Konflikt ab. Ein Speichern bedeutet
            nur „geschrieben und zurückgelesen“; eine Runtime-Aktivierung wird nicht erfunden.
          </p>
          <label>
            <input v-model="confirmed" type="checkbox" />
            Ich habe Patch und Vorschau geprüft.
          </label>
        </div>
      </div>

      <p v-if="error" class="config-editor__message config-editor__message--error" role="alert">
        <AlertTriangle :size="15" aria-hidden="true" />
        {{ error }}
      </p>
      <p v-if="success" class="config-editor__message config-editor__message--success" role="status">
        <CheckCircle2 :size="15" aria-hidden="true" />
        {{ success }}
      </p>

      <footer>
        <span>
          <ShieldCheck :size="15" aria-hidden="true" />
          Owner · operator.admin · Idempotenz · Read-back
        </span>
        <button type="button" class="nexus-button nexus-button--primary" :disabled="!canSave" @click="savePatch">
          <Loader2 v-if="saving" :size="14" class="spin" aria-hidden="true" />
          <Save v-else :size="14" aria-hidden="true" />
          Patch anwenden
        </button>
      </footer>
    </template>
  </section>
</template>

<style scoped>
.config-editor {
  display: grid;
  gap: 16px;
  min-width: 0;
  padding: 18px;
  border-color: var(--line-2);
}

.config-editor__header,
.config-editor footer {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  justify-content: space-between;
}

.config-editor__header h2,
.config-editor__grid h3 {
  margin: 0;
  color: var(--tx);
  font-family: var(--font-display-v2);
}

.config-editor__header h2 {
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 17px;
}

.config-editor__header p,
.config-editor__guard p {
  max-width: 650px;
  margin: 5px 0 0;
  color: var(--tx-2);
  font-size: 12px;
  line-height: 1.55;
}

.config-editor__eyebrow {
  display: block;
  margin-bottom: 5px;
  color: var(--a-blue);
  font-family: var(--font-mono-v2);
  font-size: 10px;
  letter-spacing: .14em;
}

.config-editor__facts {
  display: grid;
  grid-template-columns: 1.5fr .7fr 1fr;
  gap: 8px;
}

.config-editor__facts > div {
  display: grid;
  gap: 5px;
  min-width: 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--accent-wash);
}

.config-editor__facts span,
.config-editor__diff span {
  color: var(--tx-3);
  font-family: var(--font-mono-v2);
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.config-editor__facts code {
  overflow: hidden;
  color: var(--tx-2);
  font-family: var(--font-mono-v2);
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.is-valid { color: var(--st-work); }
.is-warning { color: var(--st-queue); }

.config-editor__schema-search {
  display: grid;
  gap: 6px;
}

.config-editor label {
  color: var(--tx-2);
  font-size: 11px;
  font-weight: 700;
}

.config-editor label span {
  color: var(--tx-3);
  font-weight: 500;
}

.config-editor__schema-search > div {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  padding-left: 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--glass);
}

.config-editor input:not([type='checkbox']),
.config-editor textarea {
  width: 100%;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  outline: 0;
  background: var(--glass);
  color: var(--tx);
  font: 11px/1.55 var(--font-mono-v2);
}

.config-editor input:not([type='checkbox']) {
  min-height: 38px;
  padding: 8px 10px;
}

.config-editor__schema-search input {
  border: 0;
  background: transparent;
}

.config-editor textarea {
  resize: vertical;
  padding: 10px;
}

.config-editor input:focus-visible,
.config-editor textarea:focus-visible {
  border-color: var(--a-blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--a-blue) 18%, transparent);
}

.config-editor__children {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}

.config-editor__children button {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  min-width: 0;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--glass);
  color: var(--tx);
  text-align: left;
}

.config-editor__children button:hover {
  border-color: var(--line-2);
  background: var(--accent-wash);
}

.config-editor__children span {
  display: grid;
  min-width: 0;
}

.config-editor__children strong,
.config-editor__children small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.config-editor__children small {
  margin-top: 2px;
  color: var(--tx-3);
  font: 10px var(--font-mono-v2);
}

.config-editor__grid {
  display: grid;
  grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr);
  gap: 12px;
}

.config-editor__grid > section {
  display: grid;
  align-content: start;
  gap: 7px;
  min-width: 0;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--glass);
}

.config-editor__grid h3 {
  margin-bottom: 3px;
  font-size: 13px;
}

.config-editor__validation {
  margin: 0;
  color: var(--st-block);
  font-size: 11px;
}

.config-editor__diff > div {
  display: grid;
  gap: 5px;
  min-width: 0;
}

.config-editor__diff pre {
  overflow: auto;
  max-height: 190px;
  min-height: 64px;
  margin: 0;
  padding: 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--space-0);
  color: var(--tx-2);
  font: 10px/1.5 var(--font-mono-v2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.config-editor__guard {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--status-queue-line);
  border-radius: var(--r-sm);
  background: var(--status-queue-bg);
  color: var(--st-queue);
}

.config-editor__guard strong { color: var(--tx); }
.config-editor__guard label {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 9px;
  color: var(--tx);
}

.config-editor__message {
  display: flex;
  gap: 7px;
  align-items: center;
  margin: 0;
  padding: 9px 10px;
  border-radius: var(--r-sm);
  font-size: 11px;
}

.config-editor__message--error {
  background: var(--status-block-bg);
  color: var(--st-block);
}

.config-editor__message--success {
  background: var(--status-work-bg);
  color: var(--st-work);
}

.config-editor footer {
  align-items: center;
}

.config-editor footer > span {
  display: flex;
  gap: 7px;
  align-items: center;
  color: var(--tx-3);
  font: 10px var(--font-mono-v2);
}

@media (max-width: 768px) {
  .config-editor__header,
  .config-editor footer {
    align-items: stretch;
    flex-direction: column;
  }

  .config-editor__facts,
  .config-editor__grid,
  .config-editor__children {
    grid-template-columns: minmax(0, 1fr);
  }
}
</style>
