<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { fetchGuides, fetchTopics, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBlocksStatus, fetchActiveBlocks, createBlocks as apiCreateBausteine, resetBlocksStage as apiResetBlocksStage, addBlocksResearch as apiAddResearch, requeueBlocksDead as apiRequeueDead, resetGuideBoard as apiResetGuideBoard, restartBlocksCard as apiRestartBlocksCard, resetGuideCard as apiResetGuideCard, removeGuideFormat as apiRemoveGuideFormat, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, fetchFolders, updateSource as apiUpdateQuelle } from './api.js'
import { usePolling } from './composables/usePolling.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
import BlocksOverview from './components/BlocksOverview.vue'
import GenerationView from './components/GenerationView.vue'
import GeneralExamPanel from './components/GeneralExamPanel.vue'
import PracticePanel from './components/PracticePanel.vue'

const guides = ref([])
const backendTopics = ref([])
const selectedTopic = ref(null)
const previewGuide = ref(null)
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
const sidebarSticky = ref(false)
const focusOpen = ref(false)   // block fullscreen active → lift sidebar as overlay above the focus overlay
const darkMode = ref(
  localStorage.getItem('darkMode') === null
    ? window.matchMedia('(prefers-color-scheme: dark)').matches
    : localStorage.getItem('darkMode') === 'true',
)
const EMPTY_BLOCKS = { ready: false, generating: false, progress: null, error: null, partial: false }
const blocks = ref({ ...EMPTY_BLOCKS })
const activeBlocks = ref([])
const provider = ref(localStorage.getItem('provider') || 'claude')
const providers = ref([])
const folders = ref({ projekt: [], uni: [] })   // folders for the sources picker
const mainView = ref('blocks')   // blocks | generation | general | practice | detail — exclusive main-area view
const guideBoardFormat = ref('Guide')
const viewMode = ref('compact')              // compact | erklärend — per topic, default compact
const _storedLevel = localStorage.getItem('level')
const levelView = ref(_storedLevel === 'auto' || !_storedLevel ? 'auto' : Number(_storedLevel) || 'auto')  // 'auto' | 1-4
const stats = ref(null)
const progress = ref({})
const uiError = ref(null)   // surface rejected actions (409/400)

// Run a loader, log + swallow its error (a failed background load must not break the UI).
async function guard(label, fn) {
  try { return await fn() } catch (e) { console.error(label, e) }
}

// Aktion ausführen und einen Backend-Fehler (409/400/500) in der Sidebar-Fehlerzeile
// zeigen statt als unhandled rejection zu verschlucken. → true bei Erfolg.
async function withUiError(fn) {
  uiError.value = null
  try { await fn(); return true } catch (e) { uiError.value = e.message; return false }
}

async function loadStats() {
  await guard('Failed to load stats:', async () => { stats.value = await fetchStats() })
}

function setProvider(id) {
  provider.value = id
  localStorage.setItem('provider', id)
}

async function loadFolders() {
  await guard('Failed to load folders:', async () => {
    const [projekt, uni] = await Promise.all([fetchFolders('projekt'), fetchFolders('uni')])
    folders.value = { projekt, uni }
  })
}

async function loadProviders() {
  await guard('Failed to load providers:', async () => {
    providers.value = await fetchProviders()
    const current = providers.value.find((p) => p.id === provider.value)
    if (current && !current.available) {
      const fallback = providers.value.find((p) => p.available)
      if (fallback) setProvider(fallback.id)
    }
  })
}

function applyTheme() {
  document.documentElement.classList.toggle('dark', darkMode.value)
}

function toggleDark() {
  darkMode.value = !darkMode.value
  localStorage.setItem('darkMode', darkMode.value ? 'true' : 'false')
  applyTheme()
}

applyTheme()

function toggleSidebarPin() {
  sidebarPinned.value = !sidebarPinned.value
  localStorage.setItem('sidebarPinned', sidebarPinned.value ? 'true' : 'false')
  if (sidebarPinned.value) sidebarSticky.value = false
}

function clickHoverZone() {
  sidebarSticky.value = !sidebarSticky.value
}

function onSidebarLeave() {
  if (!sidebarPinned.value) sidebarSticky.value = false
}

const topics = computed(() => backendTopics.value)

const doneByFormat = computed(() => {
  const map = {}
  for (const g of guides.value) {
    if (g.topic !== selectedTopic.value) continue
    if (g.status !== 'done') continue
    if (!map[g.format] || g.created_at > map[g.format].created_at) {
      map[g.format] = g
    }
  }
  return map
})

const latestByFormat = computed(() => {
  const map = {}
  for (const g of guides.value) {
    if (g.topic !== selectedTopic.value) continue
    if (!map[g.format] || g.created_at > map[g.format].created_at) {
      map[g.format] = g
    }
  }
  return map
})

const hasActiveGuides = computed(() =>
  guides.value.some((g) => g.status === 'queued' || g.status === 'generating'),
)

async function loadTopics() {
  try {
    backendTopics.value = await fetchTopics()
  } catch (e) {
    console.error('Failed to load topics:', e)
  }
}

// Dismissed errors stay dismissed — even across reloads (localStorage).
// Errors that aren't dismissed stay visible until the user closes them.
const dismissedErrors = ref(new Set(JSON.parse(localStorage.getItem('dismissedErrors') || '[]')))

function persistDismissed() {
  localStorage.setItem('dismissedErrors', JSON.stringify([...dismissedErrors.value]))
}

function handleDismissError(guideId) {
  dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
  persistDismissed()
}

async function loadGuides() {
  try {
    guides.value = await fetchGuides()
    // Prune IDs whose guide no longer exists as an error
    const errorIds = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
    if ([...dismissedErrors.value].some((id) => !errorIds.has(id))) {
      dismissedErrors.value = new Set([...dismissedErrors.value].filter((id) => errorIds.has(id)))
      persistDismissed()
    }
    loadStats()
  } catch (e) {
    console.error('Failed to load:', e)
  }
}

async function loadBlocks() {
  try {
    activeBlocks.value = await fetchActiveBlocks()
    if (selectedTopic.value) {
      blocks.value = await fetchBlocksStatus(selectedTopic.value)
      progress.value = await fetchTopicProgress(selectedTopic.value)
    } else {
      blocks.value = { ...EMPTY_BLOCKS }
      progress.value = {}
    }
    if (activeBlocks.value.length && !polling.running()) startPolling()
  } catch (e) {
    console.error('Failed to load blocks:', e)
  }
}

function selectTopic(topic) {
  selectedTopic.value = topic
  previewGuide.value = null
  sidebarSticky.value = false
  mainView.value = 'generation'   // topic click → generation board (guide only on pill click)
  viewMode.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'compact'
  localStorage.setItem('lastTopic', topic)
  loadBlocks()
}

// On reload, land where you were before (topic + format)
watch(previewGuide, (g) => {
  if (g) localStorage.setItem('lastFormat', g.format)
})

async function handleCancelBlocks() {
  if (!selectedTopic.value) return
  if (await withUiError(() => apiCancelBausteine(selectedTopic.value))) await loadBlocks()
}

async function handleResetBlocks() {
  if (!selectedTopic.value) return
  if (await withUiError(() => apiDeleteBausteine(selectedTopic.value))) await loadBlocks()
}

async function handleResetStage({ board, stage, restart = false }) {
  if (!selectedTopic.value) return
  uiError.value = null
  try {
    await apiResetBlocksStage(selectedTopic.value, board, stage)
    if (restart) await apiCreateBausteine(selectedTopic.value, '', provider.value, undefined, undefined, false)  // Continue: Queue abarbeiten
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadBlocks()
  if (restart) startPolling()
}

async function handleAddResearch() {
  if (!selectedTopic.value) return
  uiError.value = null
  try {
    await apiAddResearch(selectedTopic.value, provider.value)
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadBlocks()
  startPolling()
}

async function handleRequeueDead() {
  if (!selectedTopic.value) return
  const ok = await withUiError(async () => {
    await apiRequeueDead(selectedTopic.value)
    await apiCreateBausteine(selectedTopic.value, '', provider.value, undefined, undefined, false)
  })
  if (!ok) return
  await loadBlocks()
  startPolling()
}

async function handleBlocksClick({ instructions = '', research = true, qaForce = false }) {
  if (!selectedTopic.value) return
  uiError.value = null
  try {
    // research=true = Start/mehr Research anhängen; false = Continue (Queue abarbeiten).
    await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, research, qaForce)
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadBlocks()
  startPolling()
}

async function handleCreateTopic({ topic, instructions, sourceType, sourceOrt }) {
  uiError.value = null
  try {
    await apiCreateBausteine(topic, instructions, provider.value, sourceType, sourceOrt)
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadTopics()
  selectTopic(topic)
  mainView.value = 'generation'  // frisches Topic: die Boards sind das Einzige, was passiert
  startPolling()
}

async function handleUpdateSource({ topic, type, ort, spec }) {
  uiError.value = null
  try {
    await apiUpdateQuelle(topic, { type, ort, spec })
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadBlocks()  // the step display may follow the new source (e.g. link → "load source")
}

function setView(modus) {
  viewMode.value = modus
  if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
}

function setLevel(k) {
  levelView.value = k
  localStorage.setItem('level', String(k))
}

function handleOpenBlocksView() {
  if (!selectedTopic.value) return
  mainView.value = 'blocks'
  previewGuide.value = null
}

async function handleFormatClick({ format, instructions = '', abStep = null }) {
  if (!selectedTopic.value) return
  // No duplicate start: if a generation is already running for topic+format, ignore
  const running = guides.value.some(
    (g) => g.topic === selectedTopic.value && g.format === format
      && (g.status === 'generating' || g.status === 'queued'),
  )
  if (running) { handleOpenGuideBoard(format); return }
  uiError.value = null
  try {
    await apiCreate(selectedTopic.value, format, instructions, provider.value, abStep)
  } catch (e) {
    uiError.value = e.message
    return
  }
  handleOpenGuideBoard(format)  // Start → direkt aufs Live-Board
  await loadGuides()
  startPolling()
}

function handleOpenGuideBoard(format = 'Guide') {
  if (!selectedTopic.value) return
  guideBoardFormat.value = format
  mainView.value = 'generation'
  previewGuide.value = null
}

function handleOpenGeneration() {
  if (!selectedTopic.value) return
  mainView.value = 'generation'
  previewGuide.value = null
}

async function handleRemoveGuideFormat(format) {
  uiError.value = null
  try {
    await apiRemoveGuideFormat(selectedTopic.value, format)
  } catch (e) {
    uiError.value = e.message
    return
  }
  await loadGuides()
}

async function handleRestartCard(cardId) {
  uiError.value = null
  try {
    await apiRestartBlocksCard(selectedTopic.value, cardId)
    await handleBlocksClick({ research: false })  // Continue: der Flow zieht die Karte
  } catch (e) {
    uiError.value = e.message
  }
}

async function handleResetGuideCard({ format, blockNorm, abStage }) {
  uiError.value = null
  try {
    await apiResetGuideCard(selectedTopic.value, format, blockNorm, abStage)
  } catch (e) {
    uiError.value = e.message
  }
}

async function handleGuideBoardReset({ format, abStage }) {
  uiError.value = null
  try {
    await apiResetGuideBoard(selectedTopic.value, format, abStage)
  } catch (e) {
    uiError.value = e.message
  }
}

function handleGuideBoardPreview() {
  const g = doneByFormat.value[guideBoardFormat.value]
  if (g) handlePreview(g)
}

function handlePreview(guide) {
  previewGuide.value = guide
  mainView.value = 'detail'
}

function handleGeneralExam() {
  if (!selectedTopic.value) return
  mainView.value = 'general'
  previewGuide.value = null
}

function handlePractice() {
  if (!selectedTopic.value) return
  mainView.value = 'practice'
  previewGuide.value = null
}

async function handleDeleteGuide(guideId, slots = false) {
  uiError.value = null
  try {
    await deleteGuide(guideId, slots)
  } catch (e) {
    uiError.value = e.message
    return
  }
  if (previewGuide.value?.id === guideId) {
    previewGuide.value = null
  }
  await loadGuides()
}

const polling = usePolling(
  () => Promise.all([loadGuides(), loadBlocks(), loadTopics()]),
  () => hasActiveGuides.value || activeBlocks.value.length > 0,
)
const startPolling = polling.start

async function handleCancel(guideId) {
  if (await withUiError(() => apiCancel(guideId))) await loadGuides()
}

async function handleDeleteTopic(topic) {
  // Das Backend löscht Guides/Board/Kanban selbst und wehrt laufende Generierungen ab (409).
  if (!await withUiError(() => apiDeleteTopic(topic))) return
  await loadTopics()
  if (selectedTopic.value === topic) {
    selectedTopic.value = null
    previewGuide.value = null
  }
  await loadGuides()
}

onMounted(async () => {
  await Promise.all([loadGuides(), loadTopics(), loadProviders(), loadFolders()])
  const savedTopic = localStorage.getItem('lastTopic')
  if (savedTopic && topics.value.includes(savedTopic)) {
    selectTopic(savedTopic)  // lands on the blocks overview
  } else if (!selectedTopic.value && topics.value.length) {
    selectTopic(topics.value[0])
  }
})
</script>

<template>
  <div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky, 'sidebar-over-fokus': focusOpen && sidebarSticky }">
    <div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
    <div v-if="(!sidebarPinned && sidebarSticky) || (focusOpen && sidebarSticky)" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
    <TopicSidebar
      :topics="topics"
      :selectedTopic="selectedTopic"
      :stats="stats"
      :fortschritt="progress"
      :uiError="uiError"
      :doneByFormat="doneByFormat"
      :latestByFormat="latestByFormat"
      :allGuides="guides"
      :dismissedErrors="dismissedErrors"
      :blocks="blocks"
      :activeBausteine="activeBlocks"
      :pinned="sidebarPinned"
      :dark="darkMode"
      :provider="provider"
      :providers="providers"
      :folders="folders"
      :ansichtModus="viewMode"
      :stufeAnsicht="levelView"
      @setProvider="setProvider"
      @toggleDark="toggleDark"
      @setAnsicht="setView"
      @setStufe="setLevel"
      @generalExam="handleGeneralExam"
      @practice="handlePractice"
      @select="selectTopic"
      @createThema="handleCreateTopic"
      @updateSource="handleUpdateSource"
      @openBausteineView="handleOpenBlocksView"
      @openGuideBoard="handleOpenGuideBoard"
      @bausteineClick="handleBlocksClick"
      @deleteTopic="handleDeleteTopic"
      @dismissError="handleDismissError"
      @dismissUiError="uiError = null"
      @preview="handlePreview"
      @openGeneration="handleOpenGeneration"
      @togglePin="toggleSidebarPin"
      @sidebarLeave="onSidebarLeave"
    />
    <BlocksOverview
      v-if="selectedTopic && mainView === 'blocks'"
      :topic="selectedTopic"
      :generating="blocks.generating"
      :progress="blocks.progress"
      :ready="blocks.ready"
      :partial="blocks.partial"
      @close="mainView = 'detail'"
      @openGeneration="handleOpenGeneration"
    />
    <GenerationView
      v-else-if="selectedTopic && mainView === 'generation'"
      :topic="selectedTopic"
      :generating="blocks.generating"
      :progress="blocks.progress"
      :ready="blocks.ready"
      :partial="blocks.partial"
      :guideFormat="guideBoardFormat"
      @close="mainView = 'blocks'"
      @resetStage="handleResetStage"
      @restartAll="() => handleBlocksClick({ research: true })"
      @continueAll="(opts) => handleBlocksClick({ research: false, qaForce: !!(opts && opts.qaForce) })"
      @addResearch="handleAddResearch"
      @requeueDead="handleRequeueDead"
      @removeAll="handleResetBlocks"
      @cancel="handleCancelBlocks"
      @cancelGuide="handleCancel"
      @startGuide="handleFormatClick"
      @resetGuideStage="handleGuideBoardReset"
      @preview="handleGuideBoardPreview"
      @removeFormat="handleRemoveGuideFormat"
      @restartCard="handleRestartCard"
      @resetGuideCard="handleResetGuideCard"
    />
    <GeneralExamPanel
      v-else-if="selectedTopic && mainView === 'general'"
      :topic="selectedTopic"
      :provider="provider"
      @progressChanged="loadStats(); loadBlocks()"
      @fokus-active="focusOpen = $event"
    />
    <PracticePanel
      v-else-if="selectedTopic && mainView === 'practice'"
      :key="selectedTopic"
      :topic="selectedTopic"
    />
    <TopicDetail
      v-else-if="selectedTopic"
      :previewGuide="previewGuide"
      :provider="provider"
      :themaAbgeschlossen="!!progress.completed"
      :ansichtModus="viewMode"
      :stufeAnsicht="levelView"
      @progressChanged="loadStats(); loadBlocks()"
      @setAnsicht="setView"
      @open-sidebar="sidebarSticky = true"
      @fokus-active="focusOpen = $event"
    />
    <div v-else class="empty-main">
      <p>Create or select a topic in the sidebar.</p>
    </div>
  </div>
</template>

<style>
:root {
  --bg: #f8f9fb;
  --bg-preview: #f0f1f4;
  --panel: #ffffff;
  --panel-soft: #f4f5f7;
  --border: #e2e5e9;
  --border-strong: #d8dde3;
  --text: #1a1a1a;
  --text-muted: #4b5563;
  --text-faint: #9ca3af;
  --accent: #6366f1;
  --accent-hover: #4f46e5;
  --accent-soft: #ede9fe;
  --accent-border: #a5b4fc;
  --on-accent: #ffffff;
  --success: #065f46;
  --success-soft: #d1fae5;
  --success-soft-hover: #a7f3d0;
  --success-border: #34d399;
  /* 4 learning levels: green/blue/purple/gold = Beginner/Advanced/Expert/Master */
  --level-beginner: #22c55e;
  --level-advanced: #3b82f6;
  --level-expert: #8b5cf6;
  --level-master: #d4af37;
  --warning: #92400e;
  --warning-soft: #fef3c7;
  --warning-border: #fbbf24;
  --danger: #991b1b;
  --code-bg: #1e2a3a;
  --code-fg: #e6e6e6;
  --shadow: rgba(0, 0, 0, 0.12);
}

html.dark {
  --bg: #15171c;
  --bg-preview: #0e1014;
  --panel: #1c1f26;
  --panel-soft: #23262e;
  --border: #2c3038;
  --border-strong: #3a3f4a;
  --text: #e6e8ee;
  --text-muted: #9aa3b2;
  --text-faint: #6b7280;
  --accent: #6366f1;
  --accent-hover: #818cf8;
  --accent-soft: #2a2350;
  --accent-border: #4f46e5;
  --on-accent: #ffffff;
  --success: #34d399;
  --success-soft: #0f3a2e;
  --success-soft-hover: #155e45;
  --success-border: #0f805e;
  --warning: #fbbf24;
  --warning-soft: #3a2c0a;
  --warning-border: #a06a12;
  --danger: #f87171;
  --shadow: rgba(0, 0, 0, 0.5);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
}

input,
textarea {
  background: var(--panel);
  color: var(--text);
}

input::placeholder,
textarea::placeholder {
  color: var(--text-faint);
}

.layout {
  display: flex;
  height: 100dvh;
  overflow: hidden;
  position: relative;
}

.hover-zone {
  position: fixed;
  left: 0;
  top: 0;
  width: 50px;
  height: 100dvh;
  z-index: 5;
  cursor: pointer;
}

/* Invisible surface behind the open floating sidebar.
   A tap/click next to it closes it — without it there's no way out on touch. */
.sidebar-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9;
  background: transparent;
}

.layout.sidebar-floating > .sidebar {
  position: fixed;
  left: 0;
  top: 0;
  height: 100dvh;
  transform: translateX(-100%);
  transition: transform 0.2s ease;
  z-index: 10;
  box-shadow: 0 0 16px var(--shadow);
}

.layout.sidebar-floating .hover-zone:hover ~ .sidebar,
.layout.sidebar-floating > .sidebar:hover,
.layout.sidebar-floating.sidebar-open > .sidebar {
  transform: translateX(0);
}

/* Block fullscreen: lift the sidebar as an overlay ABOVE the focus overlay (z-index 40).
   Applies for pinned AND floating. */
.layout.sidebar-over-fokus > .sidebar {
  position: fixed;
  left: 0;
  top: 0;
  height: 100dvh;
  transform: translateX(0);
  z-index: 50;
  box-shadow: 0 0 16px var(--shadow);
}
.layout.sidebar-over-fokus .sidebar-backdrop {
  z-index: 49;
}

.empty-main {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 1rem;
}

</style>
