<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
import { useChatStore } from '@/stores/chat'
import ChatPanel from '@/components/ChatPanel.vue'
import WeatherCard from '@/components/WeatherCard.vue'
import { RotateCcw, Sparkles } from 'lucide-vue-next'
import {
  apiGet,
  apiPost,
  getJournalToday,
  getJournalDay,
  getJournalDays,
  triggerJournalPrep,
  listEvents,
  listJournalMoments,
  runJournalCurator,
  type EventEntry,
  type JournalMoment,
} from '@/api/client'
import { useToastStore } from '@/stores/toast'

interface WeatherDay {
  day: string
  condition: string
  high: number
  low: number
  precip_probability: number | null
  precip_mm: number | null
  windspeed_max: number
}
interface WeatherData {
  location: string
  fetched_at: string
  current_temp: number
  condition: string
  today_high: number | null
  today_low: number | null
  yesterday_high: number | null
  yesterday_low: number | null
  wind_unit?: string
  forecast: WeatherDay[]
}
interface CurrentConditions {
  temperature: number | null
  windspeed: number | null
  description: string
  precip_next_3h: number[]
  temp_unit: string
  location: string
}

const chatStore = useChatStore()
const toastStore = useToastStore()

// ── Day picker + conversation state ──────────────────────────────────────────
const days = ref<string[]>([])
const todayDate = ref<string | null>(null)
const selectedDay = ref<string | null>(null)
const dayConvId = ref<number | null>(null)
const isToday = computed(() => selectedDay.value !== null && selectedDay.value === todayDate.value)

// ── Curator + captures panel (Phase 1b) ──────────────────────────────────────
// The journal chat model has no tools — moments / tasks land via a curator
// pass (services/curator.py). The "Process captures now" button triggers
// a pass manually; the captures panel shows everything captured for the
// selected day.
const moments = ref<JournalMoment[]>([])
const momentsLoading = ref(false)
const curatorRunning = ref(false)

async function loadMoments() {
  if (!selectedDay.value) {
    moments.value = []
    return
  }
  momentsLoading.value = true
  try {
    // /api/journal/moments takes date_from + date_to (NOT `date`).
    // Passing a single ISO date for both bounds gives us the moments
    // recorded on that calendar day, which is what the captures panel
    // means by "today".
    moments.value = await listJournalMoments({
      date_from: selectedDay.value,
      date_to: selectedDay.value,
      limit: 100,
    })
  } catch {
    /* silent — moments may not exist yet */
  } finally {
    momentsLoading.value = false
  }
}

async function triggerCurator() {
  if (curatorRunning.value || !dayConvId.value) return
  curatorRunning.value = true
  try {
    const result = await runJournalCurator(dayConvId.value)
    if (result.error) {
      toastStore.show(`Curator error: ${result.error}`, 'error')
    } else {
      const captured = result.tools_succeeded
      const total = result.tools_attempted
      const detail = captured === total
        ? `${captured} tool calls`
        : `${captured}/${total} tool calls`
      // toastStore only supports success/error/warning; use success for
      // both "captured something" and the no-op case (success in the
      // sense that the curator ran cleanly).
      toastStore.show(
        captured > 0
          ? `Captured ${detail} (${result.duration_ms}ms)`
          : `Nothing new to capture (${result.duration_ms}ms)`,
        'success',
      )
    }
    await loadMoments()
  } catch (e) {
    const msg = e instanceof Error ? e.message : 'Curator failed'
    toastStore.show(`Curator failed: ${msg}`, 'error')
  } finally {
    curatorRunning.value = false
  }
}

function formatMomentTime(iso: string): string {
  try {
    return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
  } catch {
    return ''
  }
}

// ── Weather panel ────────────────────────────────────────────────────────────
const weatherData = ref<WeatherData[]>([])
const selectedWeatherIdx = ref(0)
const tempUnit = ref<string>('C')
const currentConditions = ref<CurrentConditions | null>(null)
let currentWeatherTimer: ReturnType<typeof setInterval> | null = null

async function loadCurrentConditions() {
  try {
    currentConditions.value = await apiGet<CurrentConditions>('/api/journal/weather/current')
    if (currentConditions.value?.temperature != null && weatherData.value.length > 0) {
      weatherData.value[0] = { ...weatherData.value[0], current_temp: currentConditions.value.temperature }
    }
  } catch { /* silent */ }
}

async function loadWeather() {
  try {
    const data = await apiGet<{ locations: WeatherData[]; temp_unit: string }>('/api/journal/weather')
    weatherData.value = data.locations ?? []
    tempUnit.value = data.temp_unit ?? 'C'
  } catch { /* silent */ }
}

const refreshingWeather = ref(false)
async function refreshWeather() {
  refreshingWeather.value = true
  try {
    const data = await apiPost<{ locations: WeatherData[]; temp_unit: string }>('/api/journal/weather/refresh', {})
    weatherData.value = data.locations ?? []
    tempUnit.value = data.temp_unit ?? 'C'
  } catch { /* silent */ }
  finally { refreshingWeather.value = false }
}

// ── Upcoming events ──────────────────────────────────────────────────────────
const upcomingEvents = ref<EventEntry[]>([])

interface GroupedDay {
  label: string
  dateKey: string
  events: EventEntry[]
}

const groupedEvents = computed<GroupedDay[]>(() => {
  const groups = new Map<string, EventEntry[]>()
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  for (const ev of upcomingEvents.value) {
    const d = new Date(ev.start_dt)
    const key = d.toISOString().slice(0, 10)
    if (!groups.has(key)) groups.set(key, [])
    groups.get(key)!.push(ev)
  }
  const result: GroupedDay[] = []
  for (const [key, events] of groups) {
    const d = new Date(key + 'T00:00:00')
    const diff = Math.round((d.getTime() - today.getTime()) / 86_400_000)
    let label: string
    if (diff === 0) label = 'Today'
    else if (diff === 1) label = 'Tomorrow'
    else label = d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
    result.push({ label, dateKey: key, events })
  }
  return result
})

function formatEventTime(ev: EventEntry): string {
  if (ev.all_day) return 'All day'
  const d = new Date(ev.start_dt)
  return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}

async function loadEvents() {
  try {
    // Window: today 00:00 → 14 days out. Matches the prep's framing — events
    // earlier today (already past) still surface here, grouped under "Today",
    // so the right rail aligns with what the prep references.
    const start = new Date()
    start.setHours(0, 0, 0, 0)
    const end = new Date(start)
    end.setDate(end.getDate() + 14)
    end.setHours(23, 59, 59, 999)
    upcomingEvents.value = await listEvents(start.toISOString(), end.toISOString())
  } catch { /* silent */ }
}

// ── Day load + switch ────────────────────────────────────────────────────────
async function loadDay(iso: string) {
  const payload = iso === todayDate.value ? await getJournalToday() : await getJournalDay(iso)
  if (payload.conversation) {
    dayConvId.value = payload.conversation.id
    await chatStore.fetchConversation(payload.conversation.id)
  } else {
    dayConvId.value = null
  }
}

async function loadAll() {
  try {
    const today = await getJournalToday()
    todayDate.value = today.day_date
    selectedDay.value = today.day_date
    if (today.conversation) {
      dayConvId.value = today.conversation.id
      await chatStore.fetchConversation(today.conversation.id)
    }
  } catch { /* silent */ }

  try {
    days.value = await getJournalDays()
    if (todayDate.value && !days.value.includes(todayDate.value)) {
      days.value = [todayDate.value, ...days.value]
    }
  } catch { /* silent */ }

  await Promise.all([loadWeather(), loadCurrentConditions(), loadEvents(), loadMoments()])
}

watch(selectedDay, async (iso) => {
  if (!iso) return
  // For non-today dates, loadDay handles its own day-data fetch; in both
  // cases we want the captures panel to reflect the selected day.
  try {
    if (iso !== todayDate.value) await loadDay(iso)
    await loadMoments()
  } catch { /* silent */ }
})

// ── Manual prep regeneration ─────────────────────────────────────────────────
const triggering = ref(false)
async function triggerPrep() {
  if (triggering.value) return
  triggering.value = true
  try {
    await triggerJournalPrep()
    if (_mounted) await loadAll()
  } finally {
    if (_mounted) triggering.value = false
  }
}

function dayLabel(iso: string): string {
  if (iso === todayDate.value) return 'Today'
  const d = new Date(iso + 'T00:00:00')
  return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
}

const todayBadge = computed(() => {
  return new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })
})

// ── Background refresh ───────────────────────────────────────────────────────
async function _backgroundRefreshMessages() {
  try {
    if (!_mounted || !isToday.value || !dayConvId.value) return
    await chatStore.fetchConversation(dayConvId.value)
  } catch { /* silent */ }
}

useBackgroundRefresh(
  _backgroundRefreshMessages,
  60_000,
  () => !chatStore.streaming && isToday.value && !!dayConvId.value,
)

let _mounted = true
onUnmounted(() => {
  _mounted = false
  if (currentWeatherTimer) clearInterval(currentWeatherTimer)
})

onMounted(async () => {
  await loadAll()
  currentWeatherTimer = setInterval(loadCurrentConditions, 30 * 60 * 1000)
})
</script>

<template>
  <div class="journal-root">
    <div class="journal-shell">
      <header class="journal-header">
        <div class="journal-header-left">
          <h1 class="journal-title">Journal</h1>
          <span class="journal-today-badge">{{ todayBadge }}</span>
        </div>
        <div class="journal-header-right">
          <select v-if="days.length" v-model="selectedDay" class="journal-day-select">
            <option v-for="d in days" :key="d" :value="d">{{ dayLabel(d) }}</option>
          </select>
          <button
            class="btn-trigger"
            @click="triggerPrep"
            :disabled="triggering || !isToday"
            title="Regenerate today's daily prep"
          >{{ triggering ? '…' : 'Refresh prep' }}</button>
        </div>
      </header>

      <!-- Center: chat (prep is the first assistant message inside) -->
      <div class="journal-center">
        <ChatPanel
          variant="full"
          briefingMode
          :readOnly="!isToday"
          placeholder="Tell your journal…"
          class="journal-chat-panel"
        />
      </div>

      <!-- Right: Weather + Events + News -->
      <div class="journal-right">
        <div class="weather-section" v-if="weatherData.length">
          <div class="weather-section-header">
            <div class="weather-tabs" v-if="weatherData.length > 1">
              <button
                v-for="(loc, i) in weatherData"
                :key="(loc as WeatherData).location"
                class="weather-tab"
                :class="{ active: selectedWeatherIdx === i }"
                @click="selectedWeatherIdx = i"
              >{{ (loc as WeatherData).location }}</button>
            </div>
            <button
              class="weather-refresh-btn"
              :class="{ spinning: refreshingWeather }"
              :disabled="refreshingWeather"
              @click="refreshWeather"
              title="Refresh weather"
            >
              <RotateCcw :size="16" />
            </button>
          </div>
          <WeatherCard
            :weather="weatherData[selectedWeatherIdx]"
            :temp-unit="tempUnit"
          />
        </div>

        <!-- Captures panel: shows moments extracted by the curator for the
             selected day. The chat model is tools=[]; the curator pass
             (manual button below, scheduler in phase 2) populates this. -->
        <div class="captures-section">
          <div class="panel-label-row">
            <div class="panel-label">Captures</div>
            <button
              class="captures-trigger-btn"
              :class="{ spinning: curatorRunning }"
              :disabled="curatorRunning || !dayConvId || !isToday"
              :title="isToday ? 'Run curator on this conversation' : 'Curator only runs on the current day'"
              @click="triggerCurator"
            >
              <Sparkles :size="14" />
              {{ curatorRunning ? 'Working…' : 'Process captures' }}
            </button>
          </div>
          <div v-if="momentsLoading && !moments.length" class="captures-empty">
            Loading…
          </div>
          <div v-else-if="!moments.length" class="captures-empty">
            No captures yet for {{ isToday ? 'today' : 'this day' }}.
            <span v-if="isToday">Talk in the journal, then press "Process captures".</span>
          </div>
          <ul v-else class="captures-list">
            <li v-for="m in moments" :key="m.id" class="capture-row">
              <div class="capture-time">{{ formatMomentTime(m.occurred_at) }}</div>
              <div class="capture-body">
                <div class="capture-content">{{ m.content }}</div>
                <div v-if="m.people.length || m.places.length || m.task_ids.length || m.note_ids.length" class="capture-meta">
                  <span v-if="m.people.length" class="capture-chip">
                    {{ m.people.map(p => p.title).join(', ') }}
                  </span>
                  <span v-if="m.places.length" class="capture-chip">
                    @ {{ m.places.map(p => p.title).join(', ') }}
                  </span>
                  <span v-if="m.task_ids.length" class="capture-chip capture-chip--task">
                    {{ m.task_ids.length }} task{{ m.task_ids.length === 1 ? '' : 's' }}
                  </span>
                  <span v-if="m.note_ids.length" class="capture-chip capture-chip--note">
                    {{ m.note_ids.length }} note{{ m.note_ids.length === 1 ? '' : 's' }}
                  </span>
                </div>
              </div>
            </li>
          </ul>
        </div>

        <div class="events-section" v-if="groupedEvents.length">
          <div class="panel-label-row">
            <div class="panel-label">Upcoming</div>
            <router-link to="/calendar" class="events-cal-link">Calendar →</router-link>
          </div>
          <div class="events-list">
            <div v-for="group in groupedEvents" :key="group.dateKey" class="events-day-group">
              <div class="events-day-label">{{ group.label }}</div>
              <div v-for="ev in group.events" :key="ev.id" class="event-row">
                <span class="event-dot" :style="ev.color ? { background: ev.color } : {}"></span>
                <span class="event-body">
                  <span class="event-title">{{ ev.title }}</span>
                  <span class="event-time">{{ formatEventTime(ev) }}</span>
                  <span v-if="ev.location" class="event-loc">{{ ev.location }}</span>
                </span>
              </div>
            </div>
          </div>
        </div>

      </div>
    </div>
  </div>
</template>

<style scoped>
.journal-root {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}

.journal-shell {
  display: grid;
  grid-template-columns: 1fr minmax(320px, 35%);
  grid-template-rows: auto 1fr;
  height: 100%;
  min-height: 0;
}

.journal-header {
  grid-column: 1 / -1;
  grid-row: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem 1rem 1rem;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
  gap: 1rem;
  flex-wrap: wrap;
}
.journal-header-left { display: flex; align-items: baseline; gap: 0.75rem; }
.journal-title {
  /* h1 inherits Fraunces from theme.css; weight 500 follows the doc's "two weights only" rule */
  font-size: 1.3rem;
  margin: 0;
  color: var(--color-text);
}
.journal-today-badge { font-size: 0.82rem; color: var(--color-text-muted); }
.journal-header-right { display: flex; align-items: center; gap: 0.5rem; }

.journal-day-select {
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-bg-card);
  color: var(--color-text);
  font-size: 0.82rem;
  cursor: pointer;
  font-family: inherit;
}

.btn-trigger {
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-bg-card);
  color: var(--color-text-muted);
  font-size: 0.8rem;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.15s;
  font-family: inherit;
}
.btn-trigger:hover:not(:disabled) { border-color: var(--color-primary); color: var(--color-primary); }
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Center column: prep card + chat ──────────────────────────────────────── */
.journal-center {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.journal-chat-panel { flex: 1; min-height: 0; }

/* ── Right column ─────────────────────────────────────────────────────────── */
.journal-right {
  grid-column: 2;
  grid-row: 2;
  border-left: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.weather-section { flex-shrink: 0; padding: 1rem 1rem 0.5rem; }
.weather-section :deep(.weather-card) { margin-bottom: 0; }
.weather-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}

.weather-refresh-btn {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  color: var(--color-text-muted);
  font-size: 1rem;
  cursor: pointer;
  padding: 0.2rem 0.45rem;
  line-height: 1;
  transition: all 0.15s;
}
.weather-refresh-btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
.weather-refresh-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.weather-refresh-btn.spinning { animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.weather-tabs { display: flex; gap: 0.25rem; }
.weather-tab {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: none;
  color: var(--color-text-muted);
  font-size: 0.78rem;
  font-family: inherit;
  cursor: pointer;
  transition: all 0.15s;
}
.weather-tab:hover { border-color: var(--color-primary); color: var(--color-primary); }
.weather-tab.active {
  background: color-mix(in srgb, var(--color-primary) 12%, transparent);
  border-color: var(--color-primary);
  color: var(--color-primary);
  font-weight: 500;
}

.events-section { padding: 0.75rem 1rem; border-top: 1px solid var(--color-border); }
.events-cal-link { font-size: 0.75rem; color: var(--color-text-muted); text-decoration: none; }
.events-cal-link:hover { color: var(--color-primary); }
.events-list { display: flex; flex-direction: column; gap: 0.6rem; }
.events-day-group { display: flex; flex-direction: column; gap: 0.2rem; }
.events-day-label {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.event-row { display: flex; align-items: flex-start; gap: 0.4rem; padding: 0.25rem 0.4rem; border-radius: 6px; }
.event-row:hover { background: color-mix(in srgb, var(--color-primary) 6%, transparent); }
.event-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--color-primary);
  flex-shrink: 0; margin-top: 5px;
}
.event-body { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.event-title { font-size: 0.82rem; font-weight: 500; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.event-time { font-size: 0.72rem; color: var(--color-text-muted); }
.event-loc { font-size: 0.7rem; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.panel-label {
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-primary);
  flex-shrink: 0;
}
.panel-label-row { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }

/* Captures panel — moments extracted by the curator (services/curator.py).
   Sits above the events panel; the chat is in the center column. */
.captures-section { padding: 0.75rem 1rem; border-top: 1px solid var(--color-border); }
.captures-trigger-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.72rem;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.captures-trigger-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-primary) 8%, transparent);
  color: var(--color-primary);
  border-color: var(--color-primary);
}
.captures-trigger-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.captures-trigger-btn.spinning :first-child {
  animation: captures-spin 1.2s linear infinite;
}
@keyframes captures-spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }

.captures-empty {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  font-style: italic;
  padding: 0.75rem 0;
  line-height: 1.45;
}
.captures-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  max-height: 380px;
  overflow-y: auto;
}
.capture-row {
  display: flex;
  gap: 0.6rem;
  padding: 0.4rem 0.5rem;
  border-radius: 8px;
  border-left: 2px solid color-mix(in srgb, var(--color-primary) 35%, transparent);
  background: color-mix(in srgb, var(--color-primary) 3%, transparent);
}
.capture-time {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  flex-shrink: 0;
  padding-top: 0.1rem;
  font-variant-numeric: tabular-nums;
}
.capture-body { min-width: 0; flex: 1; }
.capture-content {
  font-size: 0.85rem;
  line-height: 1.4;
  color: var(--color-text);
}
.capture-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.3rem;
}
.capture-chip {
  font-size: 0.68rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-text-muted) 12%, transparent);
  color: var(--color-text-muted);
}
.capture-chip--task { background: color-mix(in srgb, var(--color-primary) 14%, transparent); color: var(--color-primary); }
.capture-chip--note { background: color-mix(in srgb, var(--color-warning, #b88a2c) 14%, transparent); color: var(--color-warning, #b88a2c); }

@media (max-width: 900px) {
  .journal-shell { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
  .journal-center { grid-column: 1; grid-row: 2; }
  .journal-right {
    grid-column: 1; grid-row: 3;
    border-left: none;
    border-top: 1px solid var(--color-border);
    max-height: 300px;
  }
}
</style>
