<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { accountApi } from '@/api/accountApi'
import IconBtn from '@/components/ui/IconBtn.vue'

const router = useRouter()

const now = new Date()
const year = ref(now.getFullYear())
const month = ref(now.getMonth() + 1)
const unit = ref('MONTH') // DAY / WEEK / MONTH / YEAR
const tableYear = ref(now.getFullYear()) // 월별 총액 표 전용 연도
const currentYear = now.getFullYear()

const loading = ref(false)
const error = ref(null)

const budgetTotal = ref(0)
const spentTotal = ref(0)
const monthIncome = ref(0)
const monthExpense = ref(0)
const prevMonthExpense = ref(0) // 선택 월의 직전 달 지출 (대비용)
const barData = ref([]) // [{bucket, budget, expense}]
const monthlyStats = ref([]) // MONTH unit of year (income/expense)
const catType = ref('EXPENSE') // 분류별 파이차트 구분
const catData = ref([]) // [{category, total}]
const categoryList = ref([]) // 분류 목록(대/소분류 매핑용)
const trendData = ref([]) // [{month, netWorth}]
const aiBullets = ref([]) // AI 재무 코멘트(현황 관찰)
const aiTips = ref([]) // AI 절약 가이드(실행 제안)
const forecast = ref(null) // 월말 예상 지출(결정적 계산, 일회성 큰 지출 제외)
const forecastNote = ref('') // 예상 지출 근거 문장
const forecastHint = ref('') // 초반(표본 부족)엔 값 대신 안내
const FORECAST_MIN_DAYS = 10 // 이 날짜(경과일)부터 예상 지출 노출(초반 표본 부족·튐 방지)
const forecastLoading = ref(false)
const aiLoading = ref(false)
const aiNoCredit = ref(false) // FREE 회원 크레딧 소진 상태

const periodLabel = computed(() => `${year.value}년 ${String(month.value).padStart(2, '0')}월`)

function won(n) {
  return (n ?? 0).toLocaleString('ko-KR')
}
function daysInMonth(y, m) {
  return new Date(y, m, 0).getDate()
}
function range(a, b) {
  const r = []
  for (let i = a; i <= b; i++) r.push(i)
  return r
}

/* ===== 예산 대비 지출 도넛 ===== */
const R = 52
const C = 2 * Math.PI * R
const budgetRatio = computed(() => (budgetTotal.value > 0 ? Math.min(spentTotal.value / budgetTotal.value, 1) : 0))
const budgetPct = computed(() => (budgetTotal.value > 0 ? Math.round((spentTotal.value / budgetTotal.value) * 100) : 0))
const donutDash = computed(() => `${budgetRatio.value * C} ${C}`)
const donutColor = computed(() => {
  const r = budgetTotal.value > 0 ? spentTotal.value / budgetTotal.value : 0
  return r >= 1 ? '#c0392b' : r >= 0.8 ? '#e67e22' : '#2e7d32'
})

/* ===== 수입/지출 비율 ===== */
const ieTotal = computed(() => monthIncome.value + monthExpense.value)
const incomePct = computed(() => (ieTotal.value > 0 ? (monthIncome.value / ieTotal.value) * 100 : 0))

/* ===== 지난달 대비 이번달 지출 ===== */
const expenseDelta = computed(() => {
  const prev = prevMonthExpense.value
  const diff = monthExpense.value - prev
  const pct = prev > 0 ? Math.round((diff / prev) * 1000) / 10 : null
  return { prev, diff, pct }
})

/* ===== 월말 예상 지출 (결정적 계산 — 매 접속 동일값) =====
   일별 지출을 받아 '평소 하루 지출'을 구하되, 일회성 큰 지출(노트북 등)이 있는 날은
   일평균을 왜곡하므로 제외한다. 추정 = 실제 지출(일회성 포함) + 남은 날 × 평소 하루 지출. */
function robustDailyRate(days) {
  if (!days.length) return { rate: 0, dropped: 0, droppedCount: 0 }
  const sorted = [...days].sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  const median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
  // 비정기 큰 지출(대출상환·가전·여행 등)은 남은 날에 반복되지 않으므로,
  // 중앙값의 3배(또는 10만원)를 넘는 '모든' 날을 하루평균 산정에서 제외(가장 큰 1건만이 아님).
  const threshold = Math.max(median * 3, 100000)
  const canTrim = sorted.length >= 4
  const kept = []
  let dropped = 0
  let droppedCount = 0
  for (const d of sorted) {
    if (canTrim && d > threshold) {
      dropped += d
      droppedCount++
    } else {
      kept.push(d)
    }
  }
  const rate = kept.length ? kept.reduce((a, b) => a + b, 0) / kept.length : 0
  return { rate, dropped, droppedCount }
}
async function loadForecast() {
  forecast.value = null
  forecastNote.value = ''
  forecastHint.value = ''
  const isCurrent = year.value === now.getFullYear() && month.value === now.getMonth() + 1
  if (!isCurrent) return // 과거/미래 달은 추정 불필요
  const totalDays = daysInMonth(year.value, month.value)
  const elapsed = now.getDate()
  if (elapsed < 1) return
  // 초반엔 표본이 적어 추정이 크게 튀므로 일정 경과일 전에는 값 대신 안내만 표시
  if (elapsed < FORECAST_MIN_DAYS) {
    forecastHint.value = `${FORECAST_MIN_DAYS}일부터 표시돼요`
    return
  }
  forecastLoading.value = true
  try {
    const daily = await accountApi.budgetPeriod({ unit: 'DAY', year: year.value, month: month.value })
    const map = {}
    ;(daily || []).forEach((d) => (map[String(d.bucket)] = d.expense || 0))
    const days = []
    for (let d = 1; d <= elapsed; d++) days.push(map[String(d)] || 0)
    const { rate, dropped, droppedCount } = robustDailyRate(days)
    const remaining = totalDays - elapsed
    forecast.value = Math.round(monthExpense.value + rate * remaining)
    forecastNote.value =
      droppedCount > 0
        ? `대출상환 등 비정기 큰 지출 ${droppedCount}건(합계 ${won(dropped)})은 남은 ${remaining}일 추정에서 제외`
        : `최근 하루 평균 지출로 남은 ${remaining}일을 더한 추정`
  } catch {
    forecast.value = null
  } finally {
    forecastLoading.value = false
  }
}

/* ===== 막대 차트 (기간별 예산 대비 지출) ===== */
const bars = computed(() => {
  const map = {}
  barData.value.forEach((d) => (map[d.bucket] = d))
  let buckets
  if (unit.value === 'DAY') buckets = range(1, daysInMonth(year.value, month.value)).map(String)
  else if (unit.value === 'WEEK') buckets = range(1, Math.ceil(daysInMonth(year.value, month.value) / 7)).map(String)
  else if (unit.value === 'MONTH') buckets = range(1, 12).map(String)
  else buckets = barData.value.map((d) => d.bucket) // YEAR: 존재하는 연도
  return buckets.map((b) => ({
    label: unit.value === 'WEEK' ? `${b}주` : b,
    budget: map[b]?.budget || 0,
    expense: map[b]?.expense || 0,
  }))
})
const barMax = computed(() => Math.max(1, ...bars.value.flatMap((b) => [b.budget, b.expense])))

/* ===== 막대 차트 툴팁 (지출/예산 비율) ===== */
const chartRef = ref(null)
const tip = reactive({ show: false, x: 0, y: 0, label: '', budget: 0, expense: 0, ratio: 0 })
function showTip(b) {
  tip.label = b.label
  tip.budget = b.budget
  tip.expense = b.expense
  tip.ratio = b.budget > 0 ? Math.round((b.expense / b.budget) * 100) : 0
  tip.show = true
}
function moveTip(e) {
  const r = chartRef.value?.getBoundingClientRect()
  if (!r) return
  tip.x = e.clientX - r.left
  tip.y = e.clientY - r.top
}
function hideTip() {
  tip.show = false
}

/* ===== 분류별 파이차트 ===== */
const PIE_R = 46 // viewBox 120 기준, 선두께 20(hover 24) 포함해도 잘리지 않도록 (46+12=58 ≤ 60)
const PIE_C = 2 * Math.PI * PIE_R
const PIE_COLORS = ['#3b82a6', '#e67e22', '#2e7d32', '#9b59b6', '#c0392b', '#16a085', '#f39c12', '#8e44ad', '#27ae60', '#d35400', '#2980b9', '#7f8c8d']
// 분류별 집계를 대분류 기준으로 묶는다 (소분류까지 펼치면 항목이 너무 많아 스크롤이 길어짐).
// 소분류는 부모(대분류)명으로 합산, 매핑이 없으면 자기 자신을 대분류로 본다.
const catByMajor = computed(() => {
  const list = categoryList.value.filter((c) => c.type === catType.value)
  const byId = {}
  list.forEach((c) => (byId[c.id] = c))
  const toMajor = (name) => {
    const c = list.find((x) => x.name === name)
    if (c && c.parentId != null && byId[c.parentId]) return byId[c.parentId].name
    return name
  }
  const map = {}
  for (const d of catData.value) {
    const major = toMajor(d.category)
    map[major] = (map[major] || 0) + d.total
  }
  return Object.entries(map)
    .map(([category, total]) => ({ category, total }))
    .sort((a, b) => b.total - a.total)
})
const catTotal = computed(() => catByMajor.value.reduce((s, d) => s + d.total, 0))
const pieSegments = computed(() => {
  const total = catTotal.value
  if (total <= 0) return []
  let acc = 0
  return catByMajor.value.map((d, i) => {
    const len = (d.total / total) * PIE_C
    const seg = {
      category: d.category,
      total: d.total,
      pct: Math.round((d.total / total) * 100),
      color: PIE_COLORS[i % PIE_COLORS.length],
      len,
      offset: -acc,
    }
    acc += len
    return seg
  })
})
const catHover = ref(-1)

/* ===== 순자산 추이 ===== */
const trendView = computed(() => {
  const data = trendData.value
  if (!data.length) return { pts: [], line: '', area: '', zeroY: null, min: 0, max: 0 }
  const vals = data.map((d) => d.netWorth)
  const min = Math.min(...vals, 0)
  const max = Math.max(...vals, 0)
  const span = max - min || 1
  const n = data.length
  const weekly = data[0].month.length > 7 // 주간이면 'yyyy-MM-dd'
  const pts = data.map((d, i) => {
    let label, tip
    if (weekly) {
      const [, mm, dd] = d.month.split('-')
      label = `${+mm}/${+dd}`
      tip = `${+mm}/${+dd} 주`
    } else {
      label = d.month.slice(5) // MM
      tip = d.month
    }
    return {
      x: n === 1 ? 50 : (i / (n - 1)) * 100,
      y: 100 - ((d.netWorth - min) / span) * 100,
      month: tip,
      netWorth: d.netWorth,
      label,
      // 점이 많으면(주간) 축 라벨을 격주로만 표기해 겹침 방지
      showLabel: n <= 8 || i % 2 === 0 || i === n - 1,
    }
  })
  const line = pts.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ')
  const area = `${pts[0].x.toFixed(2)},100 ` + line + ` ${pts[n - 1].x.toFixed(2)},100`
  const zeroY = min < 0 && max > 0 ? 100 - ((0 - min) / span) * 100 : null
  return { pts, line, area, zeroY, min, max }
})
const trendTip = reactive({ show: false, x: 0, y: 0, month: '', netWorth: 0 })
function showTrendTip(p) {
  trendTip.month = p.month
  trendTip.netWorth = p.netWorth
  trendTip.x = p.x
  trendTip.y = p.y
  trendTip.show = true
}
function hideTrendTip() {
  trendTip.show = false
}

async function loadCat() {
  try {
    catData.value = await accountApi.categoryStats({ type: catType.value, year: year.value, month: month.value })
  } catch {
    catData.value = []
  }
}
async function loadCategoryList() {
  try {
    categoryList.value = await accountApi.categories()
  } catch {
    categoryList.value = []
  }
}
function setCatType(t) {
  catType.value = t
  loadCat()
}
async function loadTrend() {
  try {
    trendData.value = await accountApi.netWorthTrend({ unit: 'WEEK', weeks: 13 })
  } catch {
    trendData.value = []
  }
}

/* ===== 월별 총액 목록 ===== */
const monthlyRows = computed(() => {
  const map = {}
  monthlyStats.value.forEach((d) => (map[d.bucket] = d))
  return range(1, 12).map((m) => {
    const d = map[String(m)] || { income: 0, expense: 0 }
    return { month: m, income: d.income, expense: d.expense, net: d.income - d.expense }
  })
})

async function loadBar() {
  try {
    barData.value = await accountApi.budgetPeriod({ unit: unit.value, year: year.value, month: month.value })
  } catch {
    barData.value = []
  }
}

async function load() {
  loading.value = true
  error.value = null
  try {
    const pmY = month.value === 1 ? year.value - 1 : year.value
    const pmM = month.value === 1 ? 12 : month.value - 1
    const [status, summary, prevSummary] = await Promise.all([
      accountApi.budgetStatus({ year: year.value, month: month.value }),
      accountApi.summary({ year: year.value, month: month.value }),
      accountApi.summary({ year: pmY, month: pmM }),
      loadBar(),
      loadCat(),
    ])
    budgetTotal.value = status.reduce((s, x) => s + x.monthlyBudget, 0)
    spentTotal.value = status.reduce((s, x) => s + x.spent, 0)
    monthIncome.value = summary.totalIncome
    monthExpense.value = summary.totalExpense
    prevMonthExpense.value = prevSummary.totalExpense
    loadForecast() // 결정적 예상 지출(비동기)
    loadAiComment() // 집계가 준비된 뒤 AI 코멘트 생성(비동기, 화면 로딩 막지 않음)
  } catch (e) {
    error.value = e.response?.data?.message || '불러오지 못했습니다.'
  } finally {
    loading.value = false
  }
}

// 이번 달 집계만 백엔드로 보내 AI 재무 코멘트를 받는다(거래 원문 미전송, 실패해도 무중단)
async function loadAiComment() {
  aiLoading.value = true
  aiNoCredit.value = false
  aiBullets.value = []
  aiTips.value = []
  try {
    const res = await accountApi.aiComment({
      year: year.value,
      month: month.value,
      income: monthIncome.value,
      expense: monthExpense.value,
      prevExpense: prevMonthExpense.value,
      budget: budgetTotal.value,
      spent: spentTotal.value,
      categories: (catData.value || []).slice(0, 8).map((c) => ({ category: c.category, total: c.total })),
    })
    aiBullets.value = res?.bullets || []
    aiTips.value = res?.tips || []
  } catch (e) {
    if (e.response?.status === 402) {
      aiNoCredit.value = true
    }
    aiBullets.value = []
    aiTips.value = []
  } finally {
    aiLoading.value = false
  }
}

function setUnit(u) {
  unit.value = u
  loadBar()
}
function prevMonth() {
  if (month.value === 1) {
    month.value = 12
    year.value -= 1
  } else month.value -= 1
  load()
}
function nextMonth() {
  if (month.value === 12) {
    month.value = 1
    year.value += 1
  } else month.value += 1
  load()
}

// 월별 총액 표: 전용 연도
async function loadMonthly() {
  try {
    monthlyStats.value = await accountApi.stats({ unit: 'MONTH', year: tableYear.value })
  } catch {
    monthlyStats.value = []
  }
}
function prevTableYear() {
  tableYear.value -= 1
  loadMonthly()
}
function nextTableYear() {
  if (tableYear.value >= currentYear) return
  tableYear.value += 1
  loadMonthly()
}

onMounted(async () => {
  // 진입 시 밀린 정기 거래를 먼저 반영 (중복 없이)
  try {
    await accountApi.runRecurrings()
  } catch {
    // 정기 거래 반영 실패는 대시보드 조회를 막지 않는다
  }
  load()
  loadMonthly()
  loadTrend()
  loadCategoryList()
})
</script>

<template>
  <section class="dash">

    <div class="month-nav">
      <IconBtn icon="chevronLeft" title="이전 달" size="sm" @click="prevMonth" />
      <span class="period">{{ periodLabel }}</span>
      <IconBtn icon="chevronRight" title="다음 달" size="sm" @click="nextMonth" />
    </div>

    <p v-if="error" class="msg error">{{ error }}</p>
    <p v-if="loading" class="msg">불러오는 중...</p>

    <template v-else>
      <!-- 무료 회원 AI 크레딧 소진 안내 -->
      <div v-if="aiNoCredit" class="ai-comment ai-no-credit">
        <div class="ai-comment-head">✨ AI 코멘트</div>
        <p class="ai-credit-msg">AI 통계 분석 이용권이 소진되었습니다.<br>포인트 상점에서 추가 구매할 수 있어요.</p>
        <button type="button" class="ai-shop-btn" @click="router.push('/settings/shop')">포인트 상점 ›</button>
      </div>

      <!-- AI 재무 코멘트 + 절약 가이드: 이번 달 집계 해석 -->
      <div v-if="aiLoading || aiBullets.length || aiTips.length" class="ai-comment">
        <div class="ai-comment-head">✨ 이번 달 AI 코멘트</div>
        <div v-if="aiLoading" class="ai-loading">
          <span class="ai-loading-text">집계를 분석하고 있어요…</span>
          <div class="ai-loading-bar"><span></span></div>
        </div>
        <template v-else>
          <ul v-if="aiBullets.length" class="ai-comment-list">
            <li v-for="(b, i) in aiBullets" :key="i">{{ b }}</li>
          </ul>
          <div v-if="aiTips.length" class="ai-tips">
            <div class="ai-tips-head">💡 절약 가이드</div>
            <ul class="ai-comment-list">
              <li v-for="(t, i) in aiTips" :key="i">{{ t }}</li>
            </ul>
          </div>
        </template>
      </div>

      <!-- 이번 달 현황 (예산 도넛 + 수입/지출 통합) -->
      <div class="panel month-overview">
        <h2>이번 달 현황</h2>
        <div class="mo-grid">
          <div class="mo-budget">
          <div v-if="budgetTotal > 0" class="donut-wrap">
            <svg viewBox="0 0 120 120" class="donut">
              <circle cx="60" cy="60" :r="R" fill="none" stroke="var(--color-background-mute)" stroke-width="14" />
              <circle
                cx="60" cy="60" :r="R" fill="none" :stroke="donutColor" stroke-width="14"
                :stroke-dasharray="donutDash" stroke-linecap="round" transform="rotate(-90 60 60)"
              />
              <text x="60" y="56" text-anchor="middle" class="donut-pct">{{ budgetPct }}%</text>
              <text x="60" y="74" text-anchor="middle" class="donut-sub">지출/예산</text>
            </svg>
            <div class="donut-legend">
              <div>지출 <b>{{ won(spentTotal) }}</b></div>
              <div>예산 <b>{{ won(budgetTotal) }}</b></div>
              <div :class="budgetTotal - spentTotal < 0 ? 'neg' : ''">
                {{ budgetTotal - spentTotal >= 0 ? '잔여' : '초과' }} <b>{{ won(Math.abs(budgetTotal - spentTotal)) }}</b>
              </div>
            </div>
          </div>
          <p v-else class="empty">설정된 예산이 없습니다.</p>
          </div>

          <div class="ie">
            <div class="ie-row"><span>수입</span><b class="income">{{ won(monthIncome) }}</b></div>
            <div class="ie-row"><span>지출</span><b class="expense">{{ won(monthExpense) }}</b></div>
            <div v-if="expenseDelta.prev || monthExpense" class="ie-row compare">
              <span>지난달 대비 <small>지난달 {{ won(expenseDelta.prev) }}</small></span>
              <b :class="expenseDelta.diff > 0 ? 'expense' : 'income'">
                {{ expenseDelta.diff >= 0 ? '+' : '' }}{{ won(expenseDelta.diff) }}<template v-if="expenseDelta.pct !== null"> ({{ expenseDelta.pct >= 0 ? '+' : '' }}{{ expenseDelta.pct }}%)</template>
              </b>
            </div>
            <div v-if="forecastHint" class="ie-row forecast">
              <span>예상 지출 <small>{{ forecastHint }}</small></span>
              <b class="pending">—</b>
            </div>
            <div v-else-if="forecastLoading && !forecast" class="ie-row forecast">
              <span>예상 지출 <small>계산 중…</small></span>
              <b class="pending">···</b>
            </div>
            <div v-else-if="forecast" class="ie-row forecast">
              <span>예상 지출 <small>월말 추정</small></span>
              <b class="expense">{{ won(forecast) }}</b>
            </div>
            <div class="ie-bar">
              <div class="ie-fill income" :style="{ width: incomePct + '%' }"></div>
            </div>
            <div class="ie-row net"><span>수지</span><b :class="monthIncome - monthExpense < 0 ? 'expense' : 'income'">{{ won(monthIncome - monthExpense) }}</b></div>
            <p v-if="forecastNote" class="forecast-note">{{ forecastNote }}</p>
          </div>
        </div>
      </div>

      <!-- 분류별 파이차트 -->
      <div class="panel">
        <div class="panel-head">
          <h2>분류별 {{ catType === 'EXPENSE' ? '지출' : '수입' }}</h2>
          <div class="unit-tabs">
            <button type="button" :class="{ active: catType === 'EXPENSE' }" @click="setCatType('EXPENSE')">지출</button>
            <button type="button" :class="{ active: catType === 'INCOME' }" @click="setCatType('INCOME')">수입</button>
          </div>
        </div>
        <div v-if="pieSegments.length" class="pie-wrap">
          <svg viewBox="0 0 120 120" class="pie">
            <circle cx="60" cy="60" :r="PIE_R" fill="none" stroke="var(--color-background-mute)" stroke-width="20" />
            <circle
              v-for="(s, i) in pieSegments" :key="i"
              cx="60" cy="60" :r="PIE_R" fill="none" :stroke="s.color"
              :stroke-width="catHover === i ? 24 : 20"
              :stroke-dasharray="`${s.len} ${PIE_C - s.len}`" :stroke-dashoffset="s.offset"
              transform="rotate(-90 60 60)" class="pie-seg"
              @mouseenter="catHover = i" @mouseleave="catHover = -1"
            />
            <text x="60" y="57" text-anchor="middle" class="pie-center">{{ won(catTotal) }}</text>
            <text x="60" y="72" text-anchor="middle" class="pie-center-sub">총 {{ catType === 'EXPENSE' ? '지출' : '수입' }}</text>
          </svg>
          <ul class="pie-legend">
            <li v-for="(s, i) in pieSegments" :key="i" :class="{ hover: catHover === i }"
                @mouseenter="catHover = i" @mouseleave="catHover = -1">
              <span class="sw" :style="{ background: s.color }"></span>
              <span class="cat">{{ s.category }}</span>
              <span class="pct">{{ s.pct }}%</span>
              <span class="amt">{{ won(s.total) }}</span>
            </li>
          </ul>
        </div>
        <p v-if="!pieSegments.length" class="empty">{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.</p>
      </div>

      <!-- 기간별 예산 대비 지출 막대 차트 -->
      <div class="panel">
        <div class="panel-head">
          <h2>기간별 예산 대비 지출</h2>
          <div class="unit-tabs">
            <button type="button" :class="{ active: unit === 'MONTH' }" @click="setUnit('MONTH')">월별</button>
            <button type="button" :class="{ active: unit === 'YEAR' }" @click="setUnit('YEAR')">년별</button>
          </div>
        </div>
        <div class="legend-inline">
          <span class="dot budget"></span>예산 <span class="dot expense"></span>지출
        </div>
        <div v-if="bars.length" ref="chartRef" class="chart-wrap" @mousemove="moveTip" @mouseleave="hideTip">
          <div class="bar-chart">
            <div v-for="(b, i) in bars" :key="i" class="bar-col" @mouseenter="showTip(b)">
              <div class="bars">
                <div class="bar budget" :style="{ height: (b.budget / barMax) * 100 + '%' }"></div>
                <div class="bar expense" :style="{ height: (b.expense / barMax) * 100 + '%' }"></div>
              </div>
              <span class="bar-label">{{ b.label }}</span>
            </div>
          </div>
          <div v-if="tip.show" class="tooltip" :style="{ left: tip.x + 'px', top: tip.y + 'px' }">
            <div class="t-label">{{ tip.label }}{{ unit === 'YEAR' ? '년' : unit === 'MONTH' ? '월' : '' }}</div>
            <div><span class="dot budget"></span>예산 {{ won(tip.budget) }}</div>
            <div><span class="dot expense"></span>지출 {{ won(tip.expense) }}</div>
            <div class="t-ratio" :class="tip.ratio >= 100 ? 'over' : ''">예산 대비 {{ tip.ratio }}%</div>
          </div>
        </div>
        <p v-else class="empty">데이터가 없습니다.</p>
      </div>

      <!-- 순자산 추이 (최근 3개월, 주간) -->
      <div class="panel">
        <h2>순자산 추이 <span class="sub-note">최근 3개월 · 주간</span></h2>
        <div v-if="trendView.pts.length" class="trend-wrap" @mouseleave="hideTrendTip">
          <svg viewBox="0 0 100 100" preserveAspectRatio="none" class="trend-svg">
            <line v-if="trendView.zeroY !== null" x1="0" :y1="trendView.zeroY" x2="100" :y2="trendView.zeroY"
                  class="zero-line" vector-effect="non-scaling-stroke" />
            <polygon :points="trendView.area" class="trend-area" />
            <polyline :points="trendView.line" class="trend-line" vector-effect="non-scaling-stroke" />
          </svg>
          <!-- 점/호버 마커 (HTML 오버레이) -->
          <div
            v-for="(p, i) in trendView.pts" :key="i" class="trend-dot"
            :style="{ left: p.x + '%', top: p.y + '%' }"
            @mouseenter="showTrendTip(p)"
          ></div>
          <div class="trend-labels">
            <span v-for="(p, i) in trendView.pts" :key="i" class="t-axis" :style="{ left: p.x + '%' }">{{ p.showLabel ? p.label : '' }}</span>
          </div>
          <div v-if="trendTip.show" class="tooltip trend-tip" :style="{ left: trendTip.x + '%', top: trendTip.y + '%' }">
            <div class="t-label">{{ trendTip.month }}</div>
            <div :class="trendTip.netWorth < 0 ? 'neg' : ''">순자산 {{ won(trendTip.netWorth) }}</div>
          </div>
        </div>
        <p v-else class="empty">데이터가 없습니다.</p>
      </div>

      <!-- 월별 수입/지출 총액 목록 -->
      <div class="panel">
        <div class="panel-head">
          <h2>월별 총액</h2>
          <div class="year-nav">
            <IconBtn icon="chevronLeft" title="이전 연도" size="sm" @click="prevTableYear" />
            <span class="year-label">{{ tableYear }}년</span>
            <IconBtn icon="chevronRight" title="다음 연도" size="sm" :disabled="tableYear >= currentYear" @click="nextTableYear" />
          </div>
        </div>
        <table class="monthly">
          <thead>
            <tr><th>월</th><th class="num">수입</th><th class="num">지출</th><th class="num">수지</th></tr>
          </thead>
          <tbody>
            <tr v-for="r in monthlyRows" :key="r.month">
              <td>{{ r.month }}월</td>
              <td class="num income">{{ won(r.income) }}</td>
              <td class="num expense">{{ won(r.expense) }}</td>
              <td class="num" :class="r.net < 0 ? 'expense' : 'income'">{{ won(r.net) }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </template>
  </section>
</template>

<style scoped>
.dash {
  max-width: 760px;
  margin: 0 auto;
}
.head {
  margin-bottom: 0.75rem;
}
h1 {
  font-size: 1.5rem;
}
h2 {
  font-size: 1rem;
  margin-bottom: 0.75rem;
}
.month-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.month-nav button {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-background-soft);
  color: var(--color-text);
  cursor: pointer;
}
.period {
  font-size: 1.1rem;
  font-weight: 600;
  min-width: 8rem;
  text-align: center;
}
.panel {
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
}
/* AI 재무 코멘트 카드 */
.ai-comment {
  border: 1px solid hsla(160, 100%, 37%, 0.45);
  border-radius: 12px;
  background: hsla(160, 100%, 37%, 0.07);
  padding: 0.9rem 1.1rem;
  margin-bottom: 1rem;
}
.ai-comment-head {
  font-weight: 700;
  font-size: 0.95rem;
  color: hsla(160, 100%, 32%, 1);
  margin-bottom: 0.5rem;
}
.ai-loading-text {
  font-size: 0.85rem;
  opacity: 0.65;
}
.ai-loading-bar {
  height: 4px;
  margin-top: 0.45rem;
  border-radius: 999px;
  background: hsla(160, 100%, 37%, 0.15);
  overflow: hidden;
}
.ai-loading-bar > span {
  display: block;
  width: 40%;
  height: 100%;
  border-radius: 999px;
  background: hsla(160, 100%, 37%, 0.85);
  animation: ai-indeterminate 1.1s ease-in-out infinite;
}
@keyframes ai-indeterminate {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(360%); }
}
.ie-row .pending {
  opacity: 0.4;
  letter-spacing: 2px;
}
.ai-comment-list {
  margin: 0;
  padding-left: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.ai-comment-list li {
  font-size: 0.9rem;
  line-height: 1.45;
}
.ai-tips {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px dashed hsla(160, 100%, 37%, 0.35);
}
.ai-tips-head {
  font-weight: 700;
  font-size: 0.88rem;
  color: #b8860b;
  margin-bottom: 0.4rem;
}
.ai-no-credit {
  border-color: hsla(220, 80%, 60%, 0.4);
}
.ai-credit-msg {
  font-size: 0.88rem;
  color: var(--color-muted, #888);
  line-height: 1.5;
  margin: 0.4rem 0 0.7rem;
}
.ai-shop-btn {
  display: inline-block;
  padding: 6px 14px;
  border-radius: 7px;
  border: none;
  background: #2563eb;
  color: #fff;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.top-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
.top-grid .panel {
  margin-bottom: 1rem;
}
/* 이번 달 현황: 예산 도넛 + 수입/지출을 한 패널에 좌우로 */
.month-overview .mo-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem 1.5rem;
  align-items: center;
}
.mo-budget {
  min-width: 0;
}
@media (max-width: 700px) {
  .month-overview .mo-grid {
    grid-template-columns: 1fr;
    gap: 0.9rem;
  }
  .mo-budget {
    border-bottom: 1px solid var(--color-border);
    padding-bottom: 0.9rem;
  }
}
.donut-wrap {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.donut {
  width: 120px;
  height: 120px;
  flex-shrink: 0;
  overflow: visible;
}
.donut-pct {
  font-size: 20px;
  font-weight: 700;
  fill: var(--color-heading);
}
.donut-sub {
  font-size: 9px;
  fill: var(--color-text);
  opacity: 0.6;
}
.donut-legend {
  font-size: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.donut-legend .neg b {
  color: #c0392b;
}
.ie-row {
  display: flex;
  justify-content: space-between;
  padding: 0.2rem 0;
  font-size: 0.9rem;
}
.ie-row.forecast {
  opacity: 0.85;
}
.ie-row.forecast small,
.ie-row.compare small {
  font-size: 0.72rem;
  opacity: 0.6;
  font-weight: 400;
  margin-left: 0.25rem;
}
.forecast-note {
  margin: 0.3rem 0 0;
  font-size: 0.72rem;
  opacity: 0.6;
}
.ie-row.net {
  border-top: 1px solid var(--color-border);
  margin-top: 0.3rem;
  padding-top: 0.4rem;
}
.income {
  color: #2e7d32;
}
.expense {
  color: #c0392b;
}
.ie-bar {
  height: 8px;
  background: #c0392b;
  border-radius: 999px;
  overflow: hidden;
  margin: 0.4rem 0;
}
.ie-fill.income {
  height: 100%;
  background: #2e7d32;
}
.unit-tabs {
  display: flex;
  gap: 0.2rem;
}
.unit-tabs button {
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-background-soft);
  color: var(--color-text);
  font-size: 0.8rem;
  cursor: pointer;
}
.unit-tabs button.active {
  border-color: hsla(160, 100%, 37%, 1);
  color: hsla(160, 100%, 37%, 1);
  font-weight: 600;
}
.legend-inline {
  font-size: 0.78rem;
  opacity: 0.75;
  margin: 0.5rem 0;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  margin-left: 0.5rem;
}
.dot.income {
  background: #2e7d32;
}
.dot.expense {
  background: #c0392b;
}
.dot.budget {
  background: #3b82a6;
}
.chart-wrap {
  position: relative;
}
.bar-chart {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 160px;
  overflow-x: auto;
  padding-top: 0.5rem;
}
.bar-col {
  flex: 1;
  min-width: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100%;
}
.bars {
  flex: 1;
  display: flex;
  align-items: flex-end;
  gap: 1px;
  width: 100%;
  justify-content: center;
}
.bar {
  width: 45%;
  min-height: 1px;
  border-radius: 2px 2px 0 0;
}
.bar.income {
  background: #2e7d32;
}
.bar.budget {
  background: #3b82a6;
}
.bar.expense {
  background: #c0392b;
}
.tooltip {
  position: absolute;
  transform: translate(-50%, calc(-100% - 12px));
  pointer-events: none;
  background: #1e1e1e;
  color: #fff;
  border-radius: 6px;
  padding: 0.5rem 0.65rem;
  font-size: 0.78rem;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  z-index: 5;
  line-height: 1.5;
}
.tooltip .t-label {
  font-weight: 700;
  margin-bottom: 0.2rem;
}
.tooltip .dot {
  margin-left: 0;
  margin-right: 0.3rem;
}
.tooltip .t-ratio {
  margin-top: 0.25rem;
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  padding-top: 0.25rem;
  font-weight: 700;
}
.tooltip .t-ratio.over {
  color: #ff6b6b;
}
.bar-label {
  font-size: 0.65rem;
  opacity: 0.7;
  margin-top: 0.2rem;
  white-space: nowrap;
}
/* 분류별 파이차트 */
.pie-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.pie {
  width: 100%;
  max-width: 170px;
  height: auto;
  aspect-ratio: 1 / 1;
  flex-shrink: 0;
  overflow: visible;
}
.pie-seg {
  cursor: pointer;
  transition: stroke-width 0.12s ease;
}
.pie-center {
  font-size: 13px;
  font-weight: 700;
  fill: var(--color-heading);
}
.pie-center-sub {
  font-size: 8px;
  fill: var(--color-text);
  opacity: 0.6;
}
.major-rollup {
  margin-top: 0.8rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--color-border);
}
.mr-head {
  font-size: 0.78rem;
  opacity: 0.6;
  margin-bottom: 0.3rem;
}
.mr-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 0.1rem 1rem;
  margin: 0;
  padding: 0;
}
.mr-list li {
  display: flex;
  justify-content: space-between;
  font-size: 0.85rem;
  padding: 0.1rem 0;
}
.mr-cat {
  font-weight: 600;
}
.pie-legend {
  list-style: none;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 0.1rem 1rem;
  font-size: 0.85rem;
}
.pie-legend li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.3rem;
  border-radius: 4px;
}
.pie-legend li.hover {
  background: var(--color-background-mute);
}
.pie-legend .sw {
  width: 11px;
  height: 11px;
  border-radius: 2px;
  flex-shrink: 0;
}
.pie-legend .cat {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pie-legend .pct {
  opacity: 0.65;
  min-width: 2.5rem;
  text-align: right;
}
.pie-legend .amt {
  font-weight: 600;
  min-width: 5rem;
  text-align: right;
}

/* 순자산 추이 */
.sub-note {
  font-size: 0.75rem;
  font-weight: 400;
  opacity: 0.6;
}
.trend-wrap {
  position: relative;
  height: 170px;
  padding-bottom: 1.1rem;
}
.trend-svg {
  width: 100%;
  height: 150px;
  overflow: visible;
}
.trend-area {
  fill: hsla(160, 100%, 37%, 0.12);
}
.trend-line {
  fill: none;
  stroke: hsla(160, 100%, 37%, 1);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.zero-line {
  stroke: var(--color-border);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}
.trend-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--color-background);
  border: 2px solid hsla(160, 100%, 37%, 1);
  cursor: pointer;
}
.trend-dot:hover {
  transform: scale(1.3);
}
.trend-labels {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1rem;
}
.t-axis {
  position: absolute;
  transform: translateX(-50%);
  font-size: 0.62rem;
  opacity: 0.6;
  white-space: nowrap;
}
.trend-tip {
  position: absolute;
}
.trend-tip .neg {
  color: #ff6b6b;
}

.year-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.year-nav button {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-background-soft);
  color: var(--color-text);
  cursor: pointer;
}
.year-nav button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.year-label {
  font-weight: 600;
  min-width: 4rem;
  text-align: center;
}
.monthly {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}
.monthly th,
.monthly td {
  padding: 0.4rem 0.5rem;
  border-bottom: 1px solid var(--color-border);
  text-align: left;
}
.monthly .num {
  text-align: right;
}
.empty {
  font-size: 0.85rem;
  opacity: 0.6;
  padding: 1rem 0;
}
.msg {
  margin: 1rem 0;
}
.msg.error {
  color: #c0392b;
}
@media (max-width: 768px) {
  .dash {
    max-width: 100%;
  }
  .top-grid {
    grid-template-columns: 1fr;
  }
  .monthly th,
  .monthly td {
    padding: 0.4rem 0.3rem;
  }
}
</style>
