import { Router } from 'express'
import { query } from '../config/database.js'
import pool from '../config/database.js'
import { sendSuccess, sendError, parseMonth, parsePagination, parseDateRange, prevYearMonth, prevMonth } from '../middleware/error.js'
import type { AuthRequest } from '../middleware/auth.js'
import { getDataScope, scopeStoreFilter } from '../middleware/data-scope.js'

const router = Router()

router.get('/overview', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const scope = getDataScope(req)

    if (scope) {
      const { clause, params } = scopeStoreFilter(scope, [month])
      const result = await query(`
        SELECT
          count(*) AS bill_count,
          round(sum(received_total)::numeric, 2) AS received,
          round(avg(received_total)::numeric, 2) AS avg_bill_value,
          round(avg(CASE WHEN consumption > 0 THEN discount_total::numeric / consumption * 100 ELSE 0 END)::numeric, 2) AS discount_rate_pct,
          0 AS theoretical_margin_pct,
          0 AS member_bills,
          0 AS member_share_pct,
          round(sum(consumption)::numeric, 2) AS consumption,
          round(sum(discount_total)::numeric, 2) AS discount
        FROM analytics.bill_fact
        WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')${clause}
      `, params)
      sendSuccess(res, result.rows[0])
    } else {
      const result = await query(`
        SELECT o.bill_count, o.received, o.avg_bill_value, o.discount_rate_pct, o.theoretical_margin_pct, o.member_bills, o.member_share_pct,
               round(b.consumption::numeric, 2) AS consumption,
               round(b.discount::numeric, 2) AS discount
        FROM analytics.mv_overview_monthly o
        LEFT JOIN (
          SELECT round(sum(consumption)::numeric, 2) AS consumption,
                 round(sum(discount_total)::numeric, 2) AS discount
          FROM analytics.bill_fact
          WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
        ) b ON true
        WHERE o.month = $1::date
      `, [month], { skipScope: true })
      sendSuccess(res, result.rows[0])
    }
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/overview/daily', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const scope = getDataScope(req)

    if (scope) {
      const { clause, params } = scopeStoreFilter(scope, [month])
      const result = await query(`
        SELECT closed_at::date AS business_date,
               count(*) AS bill_count,
               round(sum(received_total)::numeric, 2) AS received,
               round(avg(received_total)::numeric, 2) AS avg_bill_value,
               round(avg(CASE WHEN consumption > 0 THEN discount_total::numeric / consumption * 100 ELSE 0 END)::numeric, 2) AS discount_rate_pct
        FROM analytics.bill_fact
        WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')${clause}
        GROUP BY closed_at::date
        ORDER BY business_date
      `, params)
      sendSuccess(res, result.rows)
    } else {
      const result = await query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = $1::date ORDER BY business_date`, [month], { skipScope: true })
      sendSuccess(res, result.rows)
    }
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores', async (req: AuthRequest, res) => {
  try {
    const { page, pageSize, offset } = parsePagination(req)
    const riskLevel = req.query.risk_level as string
    const quadrant = req.query.quadrant as string

    let sql = `SELECT * FROM analytics.v_store_scorecard`
    const params: any[] = []
    const conditions: string[] = []

    if (riskLevel) {
      const m = parseMonth(req)
      sql = `SELECT s.* FROM analytics.v_store_scorecard s
             JOIN analytics.mv_store_risk_rating_monthly r ON s.store_code = r.store_code
             WHERE r.month_start = $${params.length + 1} AND r.risk_level = $${params.length + 2}`
      params.push(m, riskLevel)
    }

    if (quadrant) {
      if (params.length > 0) {
        conditions.push(`b.management_quadrant = $${params.length + 1}`)
        sql = sql.replace('FROM analytics.v_store_scorecard s', 'FROM analytics.v_store_scorecard s JOIN analytics.v_store_benchmark b ON s.store_code = b.store_code')
      } else {
        sql = `SELECT s.* FROM analytics.v_store_scorecard s
               JOIN analytics.v_store_benchmark b ON s.store_code = b.store_code
               WHERE b.management_quadrant = $1`
        params.push(quadrant)
      }
    }

    sql += ` ORDER BY received DESC NULLS LAST`
    const countResult = await query(`SELECT count(*) AS total FROM (${sql}) t`, params)
    sql += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`
    params.push(pageSize, offset)

    const result = await query(sql, params)
    sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores/risk', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 ORDER BY risk_level, received DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores/priority', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT store_code, store_name, action_priority, problem_count, problem_combination,
             received, scale_tier, business_type
      FROM analytics.mv_store_action_priority_deep_monthly
      WHERE month_start = $1
      ORDER BY CASE action_priority
        WHEN 'P0-修复数据口径' THEN 1
        WHEN 'P0-综合专项整改' THEN 2
        WHEN 'P1-重点整改' THEN 3
        WHEN 'P2-单项改善' THEN 4
        WHEN '标杆候选' THEN 5
        ELSE 6
      END, received DESC
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores/quadrant', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $1 ORDER BY benchmark_score DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores/:code', async (req: AuthRequest, res) => {
  try {
    const code = req.params.code
    const month = parseMonth(req)
    const [scorecard, risk, platform, benchmark, action] = await Promise.all([
      query(`
        SELECT c002 AS store_code, c003 AS store_name,
           count(*) AS bill_count,
           count(DISTINCT c176::date) AS active_days,
           round(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 2) AS consumption,
           round(sum(COALESCE(NULLIF(c068,'')::numeric,0)), 2) AS discount,
           round(sum(COALESCE(NULLIF(c114,'')::numeric,0)), 2) AS received,
           round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / NULLIF(count(DISTINCT c176::date), 0), 2) AS avg_daily_received,
           round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / count(*), 2) AS avg_bill_value,
           round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c178,'')::numeric,0)), 0), 2) AS avg_guest_value,
           round(sum(COALESCE(NULLIF(c068,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 0) * 100, 2) AS discount_rate_pct,
           round(sum(COALESCE(NULLIF(c181,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c114,'')::numeric,0)), 0) * 100, 2) AS theoretical_margin_pct,
           round(count(*) FILTER (WHERE NULLIF(c185,'') IS NOT NULL)::numeric / count(*) * 100, 2) AS member_bill_share_pct
        FROM bill_records
        WHERE c002 = $1
          AND c176 IS NOT NULL AND c176 != ''
          AND c176::timestamp >= $2::date
          AND c176::timestamp < ($2::date + interval '1 month')
        GROUP BY c002, c003
      `, [code, month]),
      query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND store_code = $2`, [month, code]),
      query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $2 AND store_code = $1`, [code, month]),
      query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $2 AND store_code = $1`, [code, month]),
      query(`SELECT * FROM analytics.mv_store_action_priority_deep_monthly WHERE month_start = $1 AND store_code = $2`, [month, code]),
    ])

    if (scorecard.rows.length === 0) {
      const storeInfo = await query(`SELECT store_code, store_name FROM analytics.dim_store WHERE store_code = $1`, [code])
      if (storeInfo.rows.length === 0) {
        return sendError(res, 'Store not found', 404)
      }
      const si = storeInfo.rows[0] as any
      return sendSuccess(res, {
        scorecard: { store_code: si.store_code, store_name: si.store_name, bill_count: 0, active_days: 0, consumption: 0, discount: 0, received: 0, avg_daily_received: 0, avg_bill_value: 0, avg_guest_value: 0, discount_rate_pct: 0, theoretical_margin_pct: 0, member_bill_share_pct: 0 },
        risk: null,
        platform: null,
        benchmark: null,
        action: { management_quadrant: '问题门店', risk_level: null },
      })
    }

    const sc = scorecard.rows[0] as any
    const rk = risk.rows[0] as any
    const act = action.rows[0] as any

    // 计算经营象限
    const medianRev = 18642
    const medianMargin = 70.0
    const dailyRev = parseFloat(rk?.avg_daily_received) || 0
    const margin = parseFloat(rk?.theoretical_margin_pct) || 0
    const quadrant = dailyRev >= medianRev && margin >= medianMargin ? '明星门店'
      : dailyRev >= medianRev && margin < medianMargin ? '现金牛门店'
      : dailyRev < medianRev && margin >= medianMargin ? '潜力门店'
      : '问题门店'

    sendSuccess(res, {
      scorecard: sc,
      risk: rk,
      platform: platform.rows[0],
      benchmark: benchmark.rows[0],
      action: { ...act, management_quadrant: quadrant, risk_level: rk?.risk_level },
    })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/stores/:code/daily', async (req: AuthRequest, res) => {
  try {
    const code = req.params.code
    const month = parseMonth(req)
    const storeInfo = await query(`SELECT store_name FROM analytics.mv_store_risk_rating_monthly WHERE store_code = $1 AND month_start = $2::date LIMIT 1`, [code, month])
    if (storeInfo.rows.length === 0) { sendSuccess(res, []); return }
    const storeName = storeInfo.rows[0].store_name
    const result = await query(`
      SELECT c176::date AS business_date,
        count(*) AS bill_count,
        round(sum(${RECEIVED_COLS}), 2) AS received,
        round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill_value,
        round(sum(COALESCE(c068::numeric,0)) / nullif(sum(c009::numeric), 0) * 100, 2) AS discount_rate_pct
      FROM bill_records
      WHERE c003 = $1
        AND c176 IS NOT NULL AND c176 != ''
        AND c176::timestamp >= $2::date
        AND c176::timestamp < ($2::date + interval '1 month')
      GROUP BY c176::date
      ORDER BY business_date
    `, [storeName, month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/cost/comparison', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT store_code, store_name, bill_count, sales_consumption, sales_received, theoretical_cost, theoretical_profit, actual_food_cost, actual_total_cost, packaging_cost, operating_supply_cost, repair_equipment_cost, new_retail_cost, ending_inventory_amount, negative_item_lines, negative_consumption_amount, food_cost_variance, theoretical_cost_rate_pct, actual_food_cost_rate_pct, variance_to_theoretical_pct, cost_rate_gap_pct, comparison_status, variance_level FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 ORDER BY variance_to_theoretical_pct DESC NULLS LAST`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/cost/category-benchmark', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      WITH store_cat AS (
        SELECT sales_store_name AS store_name, finance_category,
          round(sum(consumption_amount)::numeric, 2) AS actual_category_cost
        FROM analytics.mv_inventory_cost_classified_monthly WHERE month_start = $1
        GROUP BY sales_store_name, finance_category
      ),
      store_sales AS (
        SELECT store_name, max(sales_received) AS sales_received
        FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1
        GROUP BY store_name
      ),
      cat_sales AS (
        SELECT sc.store_name, sc.finance_category, sc.actual_category_cost,
          CASE WHEN ss.sales_received > 0 THEN round(sc.actual_category_cost / ss.sales_received * 10000, 2) ELSE NULL END AS cost_per_10k_sales
        FROM store_cat sc JOIN store_sales ss ON sc.store_name = ss.store_name
      ),
      peer AS (
        SELECT finance_category, percentile_cont(0.5) WITHIN GROUP (ORDER BY cost_per_10k_sales) AS peer_median
        FROM cat_sales WHERE cost_per_10k_sales IS NOT NULL GROUP BY finance_category
      )
      SELECT cs.store_name, cs.finance_category, cs.actual_category_cost,
        cs.cost_per_10k_sales,
        round(pm.peer_median::numeric, 2) AS peer_median_cost_per_10k,
        CASE WHEN cs.cost_per_10k_sales IS NOT NULL AND pm.peer_median IS NOT NULL
          THEN round((cs.cost_per_10k_sales - pm.peer_median)::numeric, 2) ELSE NULL END AS excess_vs_peer_per_10k
      FROM cat_sales cs LEFT JOIN peer pm ON cs.finance_category = pm.finance_category
      ORDER BY cs.store_name
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/cost/inventory', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT a.store_code, a.store_name, a.business_type, a.scale_tier, a.estimated_inventory_days,
        t.ending_inventory_amount, t.negative_item_lines
      FROM analytics.mv_store_area_efficiency_monthly a
      LEFT JOIN analytics.mv_store_theoretical_actual_cost_monthly t
        ON a.store_code = t.store_code AND a.month_start = t.month_start
      WHERE a.month_start = $1
      ORDER BY a.estimated_inventory_days DESC NULLS LAST
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/platform/economics', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $1 ORDER BY meituan_received DESC NULLS LAST`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/member/comparison', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        CASE WHEN member_id IS NULL THEN '非会员' ELSE '会员' END AS customer_type,
        count(*) AS bill_count,
        round(sum(received_total), 2) AS received,
        round(avg(received_total), 2) AS avg_bill_value,
        round(avg(discount_total), 2) AS avg_discount,
        round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
        round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
      FROM analytics.bill_fact
      WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
      GROUP BY CASE WHEN member_id IS NULL THEN '非会员' ELSE '会员' END
      ORDER BY customer_type
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/member/repeat', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.v_store_repeat_summary_monthly WHERE month_start = $1::date ORDER BY repeat_rate_pct DESC NULLS LAST`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/sku/abc', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1 ORDER BY cumulative_revenue_share`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/sku/category', async (req: AuthRequest, res) => {
  try {
    const result = await query(`SELECT * FROM analytics.category_summary ORDER BY amount DESC`)
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/sku/attach', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_dish_pair_summary_monthly WHERE month_start = $1 ORDER BY pair_count DESC LIMIT 50`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

const RECEIVED_COLS = `COALESCE(c143::numeric,0)+COALESCE(c144::numeric,0)+COALESCE(c145::numeric,0)+COALESCE(c146::numeric,0)+COALESCE(c147::numeric,0)+COALESCE(c148::numeric,0)+COALESCE(c149::numeric,0)+COALESCE(c150::numeric,0)+COALESCE(c151::numeric,0)+COALESCE(c152::numeric,0)+COALESCE(c153::numeric,0)+COALESCE(c155::numeric,0)+COALESCE(c160::numeric,0)+COALESCE(c161::numeric,0)+COALESCE(c164::numeric,0)+COALESCE(c166::numeric,0)+COALESCE(c169::numeric,0)+COALESCE(c173::numeric,0)+COALESCE(c174::numeric,0)+COALESCE(c118::numeric,0)+COALESCE(c119::numeric,0)+COALESCE(c120::numeric,0)+COALESCE(c121::numeric,0)+COALESCE(c139::numeric,0)+COALESCE(c141::numeric,0)+COALESCE(c142::numeric,0)`

router.get('/risk/anomaly', async (req: AuthRequest, res) => {
  try {
    const { page, pageSize, offset } = parsePagination(req)
    const month = parseMonth(req)
    const storeName = req.query.store as string
    const reason = req.query.reason as string
    const cashier = req.query.cashier as string

    const conditions: string[] = [`month = to_char($1::date, 'YYYY-MM')`]
    const params: any[] = [month]
    let paramIdx = 2
    if (storeName) {
      conditions.push(`store_name = $${paramIdx}`)
      params.push(storeName)
      paramIdx++
    }
    if (reason) {
      conditions.push(`anomaly_reason = $${paramIdx}`)
      params.push(reason)
      paramIdx++
    }
    if (cashier) {
      conditions.push(`cashier ILIKE $${paramIdx}`)
      params.push(`%${cashier}%`)
      paramIdx++
    }
    const whereClause = conditions.join(' AND ')

    const countResult = await query(`SELECT count(*) AS total, round(sum(consumption),2) AS sum_consumption, round(sum(discount_total),2) AS sum_discount, round(sum(received_total),2) AS sum_received FROM mv_risk_anomaly WHERE ${whereClause}`, params)
    const result = await query(`
      SELECT store_name, bill_no, meal_period, consumption, discount_total, received_total, cashier, closed_at, anomaly_reason
      FROM mv_risk_anomaly
      WHERE ${whereClause}
      ORDER BY closed_at DESC
      LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
    `, [...params, pageSize, offset])
    sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize, sum_consumption: countResult.rows[0].sum_consumption, sum_discount: countResult.rows[0].sum_discount, sum_received: countResult.rows[0].sum_received })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/risk/zero-received', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const storeName = req.query.store as string
    let sql = `
      SELECT store_name, bill_no, meal_period, consumption, discount_total,
        0 AS received_total, cashier, closed_at, zero_received_type
      FROM mv_risk_zero
      WHERE month = to_char($1::date, 'YYYY-MM')
    `
    const params: any[] = [month]
    if (storeName) {
      sql += ` AND store_name = $2`
      params.push(storeName)
    }
    sql += ` ORDER BY consumption DESC LIMIT 200`
    const result = await query(sql, params)
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/risk/cashier', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT store_name, cashier, bill_count,
        received,
        (anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) AS anomaly_bills,
        round((anomaly_no_received + anomaly_discount_over + anomaly_unbalanced)::numeric / bill_count * 100, 2) AS anomaly_rate_pct,
        consumption_total
      FROM mv_risk_cashier
      WHERE month = to_char($1::date, 'YYYY-MM') AND cashier IS NOT NULL AND cashier != ''
      ORDER BY (anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) DESC NULLS LAST
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/marketing/plans', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT marketing_plan,
        count(*) AS bill_count,
        round(sum(consumption)::numeric, 2) AS consumption,
        round(sum(discount_total)::numeric, 2) AS discounts,
        round(sum(received_total)::numeric, 2) AS received,
        round(avg(received_total), 2) AS avg_bill_value,
        round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
        round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
      FROM analytics.bill_fact
      WHERE marketing_plan IS NOT NULL AND marketing_plan != ''
        AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
      GROUP BY marketing_plan
      ORDER BY received DESC
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/benchmark/composite', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $1 ORDER BY benchmark_score DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/time/weekday', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT weekday_no, weekday, bill_count, received, avg_bill_value, avg_duration_minutes
      FROM mv_time_weekday
      WHERE month = to_char($1::date, 'YYYY-MM')
      ORDER BY weekday_no
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/time/hourly', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT closing_hour, bill_count, received, avg_bill_value, avg_duration_minutes
      FROM mv_time_hourly
      WHERE month = to_char($1::date, 'YYYY-MM')
      ORDER BY closing_hour
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/channel', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT business_date, cash, alipay, wechat, meituan, unionpay, douyin, credit, jd_delivery, meituan_delivery, taobao_delivery
      FROM mv_channel_daily
      WHERE month = to_char($1::date, 'YYYY-MM')
      ORDER BY business_date
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

router.get('/data-quality', async (req: AuthRequest, res) => {
  try {
    const billStats = await query(`
      SELECT
        count(*) AS total_bills,
        count(*) FILTER (WHERE bill_no IS NULL OR bill_no = '') AS missing_bill_no,
        count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS missing_store_code,
        count(*) FILTER (WHERE consumption = 0 OR consumption IS NULL) AS zero_consumption,
        count(*) FILTER (WHERE received_total < 0) AS negative_received,
        count(*) FILTER (WHERE received_total = 0 OR received_total IS NULL) AS zero_received,
        count(*) FILTER (WHERE discount_total > consumption AND consumption > 0) AS discount_gt_consumption,
        count(DISTINCT store_code) AS store_count,
        min(closed_at)::text AS min_date,
        max(closed_at)::text AS max_date
      FROM analytics.bill_fact
    `)
    const dishStats = await query(`
      SELECT
        count(*) AS total_dish_records,
        count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS dish_missing_store,
        count(*) FILTER (WHERE dish_name IS NULL OR dish_name = '') AS dish_missing_dish
      FROM public.dish_sales_details
    `)
    const month = parseMonth(req)
    const inventoryStats = await query(`
      SELECT
        count(*) FILTER (WHERE consumption_amount < 0) AS negative_consumption_count,
        round(sum(consumption_amount) FILTER (WHERE consumption_amount < 0)::numeric, 2) AS negative_consumption_amount
      FROM analytics.mv_inventory_cost_classified_monthly
      WHERE month_start = $1
    `, [month])
    const result = { ...billStats.rows[0], ...dishStats.rows[0], ...inventoryStats.rows[0] }
    sendSuccess(res, result)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// ============================================================
// 门店详情页扩展 API (Phase 9)
// ============================================================

router.get('/stores/:code/meal-period', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const code = req.params.code
    const result = await query(`
      WITH network AS (
        SELECT meal_period,
           count(*) AS network_bills,
           (sum(received_total) / NULLIF(count(*), 0)) AS network_avg_bill
        FROM analytics.bill_fact
        WHERE meal_period IS NOT NULL
          AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
        GROUP BY meal_period
      ), store_meal AS (
        SELECT store_code, store_name, meal_period,
           count(*) AS bill_count,
           sum(received_total) AS received,
           (sum(received_total) / NULLIF(count(*), 0)) AS avg_bill
        FROM analytics.bill_fact
        WHERE meal_period IS NOT NULL
          AND store_code = $2
          AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
        GROUP BY store_code, store_name, meal_period
      )
      SELECT s.store_code, s.store_name, s.meal_period,
         s.bill_count,
         round(s.received, 2) AS received,
         round(s.avg_bill, 2) AS avg_bill,
         round(n.network_avg_bill, 2) AS network_avg_bill,
         round(GREATEST(n.network_avg_bill - s.avg_bill, 0) * s.bill_count, 2) AS avg_bill_uplift_scenario,
         round((s.avg_bill / n.network_avg_bill - 1) * 100, 2) AS vs_network_pct
      FROM store_meal s
      JOIN network n ON s.meal_period = n.meal_period
      ORDER BY s.bill_count DESC
    `, [month, code])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const [result, companyResult] = await Promise.all([
      query(`SELECT * FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
      query(`
        SELECT 
          SUM(consumption) as total_consumption,
          SUM(lanzhou_noodle) as total_noodle,
          SUM(western_staple) as total_western,
          SUM(delivery_package) as total_delivery,
          SUM(cold_dishes) as total_cold,
          SUM(silk_road_food) as total_silk
        FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1
      `, [month]),
    ])
    sendSuccess(res, { store: result.rows[0], company: companyResult.rows[0] })
  } catch (err: any) { sendError(res, err.message) }
})

router.get('/stores/:code/cost', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const [costResult, catResult] = await Promise.all([
      query(`SELECT * FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
      query(`
        WITH store_cat AS (
          SELECT finance_category AS category_name,
            round(sum(consumption_amount)::numeric, 2) AS consumption_amount,
            round(sum(consumption_amount)::numeric / nullif(sum(sum(consumption_amount)) OVER (), 0) * 100, 1) AS actual_cost_rate_pct
          FROM analytics.mv_inventory_cost_classified_monthly
          WHERE month_start = $1 AND sales_store_code = $2 AND finance_category IS NOT NULL
          GROUP BY finance_category
          ORDER BY sum(consumption_amount) DESC
        ),
        company_cat AS (
          SELECT finance_category,
            round(sum(consumption_amount)::numeric / nullif(sum(sum(consumption_amount)) OVER (), 0) * 100, 1) AS benchmark_rate_pct
          FROM analytics.mv_inventory_cost_classified_monthly
          WHERE month_start = $1 AND finance_category IS NOT NULL
          GROUP BY finance_category
        )
        SELECT s.category_name, s.consumption_amount, s.actual_cost_rate_pct,
          COALESCE(c.benchmark_rate_pct, 0) AS benchmark_rate_pct,
          round(s.actual_cost_rate_pct - COALESCE(c.benchmark_rate_pct, 0), 1) AS variance_pct
        FROM store_cat s
        LEFT JOIN company_cat c ON s.category_name = c.finance_category
        ORDER BY s.consumption_amount DESC
      `, [month, req.params.code]),
    ])
    sendSuccess(res, { cost: costResult.rows[0], categories: catResult.rows })
  } catch (err: any) { sendError(res, err.message) }
})

router.get('/stores/:code/member', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const [oppResult, repeatResult, monthlyResult] = await Promise.all([
      query(`SELECT * FROM analytics.mv_store_member_opportunity_monthly WHERE month_start = $2 AND store_code = $1`, [req.params.code, month]),
      query(`SELECT * FROM analytics.mv_store_repeat_summary_monthly WHERE store_code = $1 AND month_start = $2::date`, [req.params.code, month]),
      query(`
        SELECT store_code, store_name, count(*) as member_count, 
          sum(orders) as total_orders, sum(received) as total_received,
          avg(orders) as avg_orders, avg(received) as avg_received
        FROM analytics.v_store_member_monthly_activity 
        WHERE store_code = $1 AND month_start = $2::date
        GROUP BY store_code, store_name
      `, [req.params.code, month]),
    ])
    sendSuccess(res, { opportunity: oppResult.rows[0], repeat: repeatResult.rows[0], monthly: monthlyResult.rows[0] })
  } catch (err: any) { sendError(res, err.message) }
})

router.get('/stores/:code/anomalies', async (req: AuthRequest, res) => {
  try {
    const { page, pageSize, offset } = parsePagination(req)
    const month = parseMonth(req)
    const code = req.params.code
    const countResult = await query(`SELECT count(*) AS total FROM mv_risk_anomaly WHERE store_name = (SELECT store_name FROM analytics.dim_store WHERE store_code = $1) AND month = to_char($2::date, 'YYYY-MM')`, [code, month])
    const result = await query(`SELECT * FROM mv_risk_anomaly WHERE store_name = (SELECT store_name FROM analytics.dim_store WHERE store_code = $1) AND month = to_char($2::date, 'YYYY-MM') ORDER BY closed_at DESC LIMIT $3 OFFSET $4`, [code, month, pageSize, offset])
    sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize })
  } catch (err: any) { sendError(res, err.message) }
})

// 门店人力排班分析
router.get('/stores/:code/staffing', async (req: AuthRequest, res) => {
  try {
    const code = req.params.code
    const monthStart = parseMonth(req)

    // 1. 门店人工成本概览
    const expenseResult = await query(`
      SELECT
        sales_store_code, sales_store_name,
        wage_expense,
        wage_rate_pct,
        received,
        bill_count,
        area_sqm,
        round(received / nullif(wage_expense, 0), 2) AS revenue_per_wage,
        round(wage_expense / nullif(bill_count, 0), 2) AS wage_per_bill
      FROM analytics.mv_store_operating_expense_monthly
      WHERE report_month = $1::date AND sales_store_code = $2
    `, [monthStart, code])

    // 2. 公司均值
    const avgResult = await query(`
      SELECT
        round(avg(wage_expense), 2) AS avg_wage,
        round(avg(wage_rate_pct), 2) AS avg_wage_rate,
        round(avg(received), 2) AS avg_received,
        round(avg(bill_count), 0) AS avg_bills,
        round(avg(received / nullif(wage_expense, 0)), 2) AS avg_rev_per_wage
      FROM analytics.mv_store_operating_expense_monthly
      WHERE report_month = $1::date AND wage_expense > 0 AND wage_rate_pct <= 100
    `, [monthStart])

    // 3. 按星期分析营收和账单
    const weekdayResult = await query(`
      SELECT
        extract(dow FROM business_date)::int AS dow,
        CASE extract(dow FROM business_date)
          WHEN 0 THEN '周日' WHEN 1 THEN '周一' WHEN 2 THEN '周二'
          WHEN 3 THEN '周三' WHEN 4 THEN '周四' WHEN 5 THEN '周五'
          WHEN 6 THEN '周六'
        END AS weekday,
        count(*) AS days,
        round(avg(received), 2) AS avg_received,
        round(avg(bill_count), 0) AS avg_bills,
        round(max(received), 2) AS max_received,
        round(min(received), 2) AS min_received,
        round(sum(received), 2) AS total_received,
        round(sum(bill_count), 0) AS total_bills
      FROM analytics.v_store_daily
      WHERE store_code = $1
        AND business_date >= $2::date AND business_date < $2::date + interval '1 month'
      GROUP BY 1, 2
      ORDER BY 1
    `, [code, monthStart])

    // 4. 餐段分布
    const mealResult = await query(`
      SELECT meal_period, bill_count, received, round(avg_bill, 2) AS avg_bill
      FROM analytics.v_store_meal_opportunity
      WHERE store_code = $1
      ORDER BY received DESC
    `, [code])

    // 5. 日营收明细（用于排班热力图）
    const dailyResult = await query(`
      SELECT
        business_date,
        to_char(business_date, 'Dy') AS dow_short,
        extract(dow FROM business_date)::int AS dow,
        round(received, 2) AS received,
        bill_count,
        round(received / nullif(bill_count, 0), 2) AS avg_bill_value
      FROM analytics.v_store_daily
      WHERE store_code = $1
        AND business_date >= $2::date AND business_date < $2::date + interval '1 month'
      ORDER BY business_date
    `, [code, monthStart])

    const store = expenseResult.rows[0] || {}
    const avg = avgResult.rows[0] || {}
    const weekdays = weekdayResult.rows
    const meals = mealResult.rows
    const daily = dailyResult.rows

    // 6. 计算排班建议
    const avgWageRate = Number(avg.avg_wage_rate) || 25
    const storeWageRate = Number(store.wage_rate_pct) || 0
    const suggestedWageRate = Math.max(20, Math.round((avgWageRate - 3) * 10) / 10)
    const suggestedWage = Number(store.received || 0) * suggestedWageRate / 100
    const wageSavings = Number(store.wage_expense || 0) - suggestedWage
    const savingsPct = Number(store.wage_expense) > 0
      ? Math.round((wageSavings / Number(store.wage_expense)) * 100 * 10) / 10
      : 0

    // 按星期分布生成排班建议
    const maxDayRev = Math.max(...weekdays.map((w: any) => Number(w.avg_received)))
    const minDayRev = Math.min(...weekdays.map((w: any) => Number(w.avg_received)))
    const avgDayRev = weekdays.reduce((s: number, w: any) => s + Number(w.avg_received), 0) / (weekdays.length || 1)

    const staffingSuggestions = weekdays.map((w: any) => {
      const ratio = Number(w.avg_received) / avgDayRev
      let level: string, action: string
      if (ratio > 1.1) {
        level = '高峰'
        action = '建议满编排班，确保出餐效率'
      } else if (ratio < 0.85) {
        level = '低谷'
        action = '建议减少1-2人排班，控制人力成本'
      } else {
        level = '正常'
        action = '建议标准排班'
      }
      return {
        ...w,
        staffing_level: level,
        staffing_action: action,
        suggested_headcount_ratio: Math.round(ratio * 100),
      }
    })

    // 餐段排班建议
    const totalMealBills = meals.reduce((s: number, m: any) => s + Number(m.bill_count), 0)
    const mealSuggestions = meals.map((m: any) => {
      const share = totalMealBills > 0 ? Number(m.bill_count) / totalMealBills : 0
      let level: string, action: string
      if (share > 0.4) {
        level = '主力时段'
        action = '建议安排全量人员，前厅后厨满编'
      } else if (share > 0.2) {
        level = '次高峰'
        action = '建议安排80%人员，保证出餐速度'
      } else if (share > 0.05) {
        level = '辅助时段'
        action = '建议安排50%人员，控制人力投入'
      } else {
        level = '低谷时段'
        action = '建议安排值班人员即可，可安排备料和清洁'
      }
      return {
        ...m,
        bill_share_pct: Math.round(share * 1000) / 10,
        staffing_level: level,
        staffing_action: action,
      }
    })

    // 综合诊断
    const diagnosis: string[] = []
    if (storeWageRate > 35) {
      diagnosis.push(`人工成本率 ${storeWageRate}% 严重偏高，公司均值 ${avgWageRate}%，需立即优化排班`)
    } else if (storeWageRate > avgWageRate + 3) {
      diagnosis.push(`人工成本率 ${storeWageRate}% 高于公司均值 ${avgWageRate}%，建议优化排班结构`)
    } else if (storeWageRate > 0) {
      diagnosis.push(`人工成本率 ${storeWageRate}% 在合理范围内，公司均值 ${avgWageRate}%`)
    }

    if (maxDayRev > 0 && minDayRev > 0 && maxDayRev / minDayRev > 1.5) {
      const fmt = (v: number) => `¥${Math.round(v).toLocaleString()}`
      diagnosis.push(`日营收波动大（最高 ${fmt(maxDayRev)} vs 最低 ${fmt(minDayRev)}），建议差异化排班`)
    }

    if (wageSavings > 0) {
      const fmt = (v: number) => `¥${Math.round(v).toLocaleString()}`
      diagnosis.push(`按建议人工率 ${suggestedWageRate}% 测算，可节省人工成本 ${fmt(wageSavings)}（${savingsPct}%）`)
    }

    const peakMeal = mealSuggestions[0]
    if (peakMeal && peakMeal.bill_share_pct > 40) {
      diagnosis.push(`${peakMeal.meal_period}为绝对主力（占 ${peakMeal.bill_share_pct}%），建议集中人力保障`)
    }

    sendSuccess(res, {
      store,
      company_avg: avg,
      weekdays: staffingSuggestions,
      meals: mealSuggestions,
      daily,
      summary: {
        wage_expense: store.wage_expense,
        wage_rate_pct: store.wage_rate_pct,
        received: store.received,
        suggested_wage: Math.round(suggestedWage * 100) / 100,
        wage_savings: Math.round(wageSavings * 100) / 100,
        savings_pct: savingsPct,
        revenue_per_wage: store.revenue_per_wage,
        wage_per_bill: store.wage_per_bill,
        avg_wage_rate: avg.avg_wage_rate,
        avg_rev_per_wage: avg.avg_rev_per_wage,
      },
      diagnosis,
    })
  } catch (err: any) { sendError(res, err.message) }
})

// 区域汇总
router.get('/region/summary', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const scope = getDataScope(req)
    const { clause, params } = scopeStoreFilter(scope, [month])
    const result = await query(`
      SELECT d.region,
        count(DISTINCT b.store_code) AS store_count,
        round(COALESCE(sum(b.consumption), 0)::numeric, 2) AS total_consumption,
        round(COALESCE(sum(b.discount_total), 0)::numeric, 2) AS total_discount,
        round(COALESCE(sum(b.received_total), 0)::numeric, 2) AS total_received,
        count(*) AS total_bill_count,
        round(avg(b.received_total), 2) AS avg_bill_value,
        round(sum(b.discount_total) / NULLIF(sum(b.consumption), 0) * 100, 2) AS avg_discount_rate,
        round(sum(b.theoretical_profit) / NULLIF(sum(b.received_total), 0) * 100, 2) AS avg_margin_rate,
        0 AS avg_anomaly_rate,
        count(DISTINCT CASE WHEN r.risk_level = '红色' THEN b.store_code END) AS red_count,
        count(DISTINCT CASE WHEN r.risk_level = '黄色' THEN b.store_code END) AS yellow_count,
        count(DISTINCT CASE WHEN r.risk_level = '绿色' THEN b.store_code END) AS green_count
      FROM analytics.bill_fact b
        JOIN analytics.dim_store d ON d.store_code = b.store_code
        LEFT JOIN analytics.mv_store_risk_rating_monthly r ON r.store_code = b.store_code AND r.month_start = $1::date
      WHERE d.region IS NOT NULL AND d.region <> ''
        AND b.closed_at >= $1::date AND b.closed_at < ($1::date + interval '1 month')${clause.replace(' AND store_code', ' AND b.store_code')}
      GROUP BY d.region
      ORDER BY total_received DESC
    `, params, { skipScope: true })
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 门店选址分析 — 门店选址画像
router.get('/site-selection/profile', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_site_profile_monthly WHERE month_start = $1 AND business_type='标准门店' AND received>0 ORDER BY received DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 门店选址分析 — 分段基准（场景×面积）
router.get('/site-selection/segment-benchmark', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_site_segment_benchmark_monthly WHERE month_start = $1 ORDER BY avg_received_per_sqm DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 门店选址分析 — 复制评分
router.get('/site-selection/replication', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_site_replication_monthly WHERE month_start = $1 ORDER BY site_replication_score DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 门店选址分析 — 重叠风险
router.get('/site-selection/overlap-risk', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_store_overlap_risk_monthly WHERE month_start = $1 ORDER BY distance_km`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 门店选址分析 — 区域基准
router.get('/site-selection/district-benchmark', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`SELECT * FROM analytics.mv_district_site_benchmark_monthly WHERE month_start = $1 ORDER BY total_received DESC`, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 利润瀑布数据
router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      WITH bill_discount AS (
        SELECT store_code,
          round(sum(discount_total)::numeric, 2) AS discount
        FROM analytics.bill_fact
        WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
        GROUP BY store_code
      ),
      full_scope AS (
        SELECT
          r.received,
          e.consumption,
          COALESCE(bd.discount, e.consumption - e.received) AS discount,
          e.actual_food_cost,
          e.wage_expense,
          e.rent_expense,
          e.utility_expense,
          e.dorm_expense,
          e.delivery_commission_expense,
          e.card_fee_expense,
          e.repair_clean_expense,
          e.operating_expense,
          CASE WHEN e.operating_expense IS NOT NULL THEN true ELSE false END AS has_expense
        FROM analytics.mv_store_risk_rating_monthly r
        LEFT JOIN analytics.mv_store_operating_expense_monthly e
          ON r.store_code = e.sales_store_code AND e.report_month = $1::date
        LEFT JOIN bill_discount bd ON bd.store_code = r.store_code
        WHERE r.month_start = $1 AND r.received IS NOT NULL AND e.operating_expense IS NOT NULL
      )
      SELECT
        round(sum(consumption)::numeric, 2) AS consumption,
        round(sum(discount)::numeric, 2) AS discount,
        round(sum(received)::numeric, 2) AS received,
        round(sum(actual_food_cost)::numeric, 2) AS food_cost,
        round(sum(wage_expense)::numeric, 2) AS wage,
        round(sum(rent_expense)::numeric, 2) AS rent,
        round(sum(utility_expense)::numeric, 2) AS utility,
        round(sum(dorm_expense)::numeric, 2) AS dorm,
        round(sum(delivery_commission_expense)::numeric, 2) AS commission,
        round(sum(card_fee_expense + repair_clean_expense)::numeric, 2) AS other_expense,
        round(sum(operating_expense)::numeric, 2) AS total_expense,
        round(sum(received) - sum(actual_food_cost) - sum(operating_expense), 2) AS store_contribution,
        count(*) AS covered_stores
      FROM full_scope
    `, [month])
    sendSuccess(res, result.rows[0])
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 门店利润排名
router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        r.store_code,
        r.store_name,
        round(r.received::numeric, 2) AS received,
        round(e.actual_food_cost::numeric, 2) AS food_cost,
        round(e.operating_expense::numeric, 2) AS expense,
        round((r.received - e.actual_food_cost - e.operating_expense)::numeric, 2) AS store_contribution,
        round((r.received - e.actual_food_cost - e.operating_expense) / nullif(r.received, 0) * 100, 2) AS contribution_margin_pct,
        r.risk_level
      FROM analytics.mv_store_risk_rating_monthly r
      LEFT JOIN analytics.mv_store_operating_expense_monthly e
        ON r.store_code = e.sales_store_code AND e.report_month = $1::date
      WHERE r.month_start = $1 AND r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
      ORDER BY store_contribution DESC
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 利润机会池
router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      WITH risk AS (
        SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1
      ),
      sku_abc AS (
        SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1
      ),
      standard_stores AS (
        SELECT r.store_code, r.store_name, r.received,
               e.actual_food_cost, e.operating_expense,
               e.wage_expense, e.utility_expense,
               r.theoretical_margin_pct, r.discount_rate_pct
        FROM risk r
        LEFT JOIN analytics.mv_store_operating_expense_monthly e
          ON r.store_code = e.sales_store_code AND e.report_month = $1::date
        WHERE r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
      ),
      cost_diff AS (
        SELECT
          round(sum(actual_food_cost)::numeric, 2) AS actual_cost,
          round(sum(received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS theoretical_cost
        FROM standard_stores
      ),
      cost_diff_stores AS (
        SELECT store_name, received,
          round(actual_food_cost::numeric, 2) AS actual_food_cost,
          round((received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS theoretical_cost,
          round((actual_food_cost - received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS diff_amount,
          round((actual_food_cost / nullif(received, 0) * 100)::numeric, 2) AS actual_cost_rate,
          round((received * (1 - COALESCE(theoretical_margin_pct, 0) / 100) / nullif(received, 0) * 100)::numeric, 2) AS theoretical_cost_rate
        FROM standard_stores
        WHERE actual_food_cost IS NOT NULL
        ORDER BY (actual_food_cost - received * (1 - COALESCE(theoretical_margin_pct, 0) / 100)) DESC
        LIMIT 5
      ),
      labor AS (
        SELECT
          round(sum(wage_expense)::numeric, 2) AS total_wage,
          round(sum(received)::numeric, 2) AS total_received
        FROM standard_stores
      ),
      labor_stores AS (
        SELECT store_name, received,
          round(wage_expense::numeric, 2) AS wage,
          round((wage_expense / nullif(received, 0) * 100)::numeric, 2) AS wage_rate
        FROM standard_stores
        WHERE wage_expense IS NOT NULL
        ORDER BY wage_expense / nullif(received, 0) DESC
        LIMIT 5
      ),
      energy AS (
        SELECT
          round(sum(utility_expense)::numeric, 2) AS total_utility,
          round(sum(received)::numeric, 2) AS total_received
        FROM standard_stores
      ),
      energy_stores AS (
        SELECT store_name, received,
          round(utility_expense::numeric, 2) AS utility,
          round((utility_expense / nullif(received, 0) * 100)::numeric, 2) AS utility_rate
        FROM standard_stores
        WHERE utility_expense IS NOT NULL
        ORDER BY utility_expense / nullif(received, 0) DESC
        LIMIT 5
      ),
      discount AS (
        SELECT
          count(*) FILTER (WHERE discount_rate_pct > 25) AS high_discount_stores,
          round(sum(CASE WHEN discount_rate_pct > 25 THEN received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0) ELSE 0 END)::numeric, 2) AS theoretical_saving
        FROM risk
        WHERE received IS NOT NULL AND received > 0
      ),
      discount_stores AS (
        SELECT store_name, received,
          round(discount_rate_pct::numeric, 2) AS discount_rate,
          round((received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0))::numeric, 2) AS potential_saving
        FROM risk
        WHERE received IS NOT NULL AND received > 0 AND discount_rate_pct > 25
        ORDER BY discount_rate_pct DESC
        LIMIT 5
      ),
      platform AS (
        SELECT
          round(sum(delivery_commission_expense)::numeric, 2) AS total_commission,
          round(sum(received)::numeric, 2) AS platform_received
        FROM analytics.mv_store_operating_expense_monthly
        WHERE report_month = $1::date AND delivery_commission_expense IS NOT NULL
      ),
      platform_stores AS (
        SELECT e.sales_store_code AS store_code, e.received,
          round(e.delivery_commission_expense::numeric, 2) AS commission,
          round((e.delivery_commission_expense / nullif(e.received, 0) * 100)::numeric, 2) AS commission_rate
        FROM analytics.mv_store_operating_expense_monthly e
        WHERE e.report_month = $1::date AND e.delivery_commission_expense IS NOT NULL
        ORDER BY e.delivery_commission_expense / nullif(e.received, 0) DESC
        LIMIT 5
      ),
      sku AS (
        SELECT
          count(*) FILTER (WHERE received_amount < 1000 AND bill_count < 30) AS low_sku_count,
          round(sum(received_amount) FILTER (WHERE received_amount < 1000 AND bill_count < 30)::numeric, 2) AS low_sku_revenue
        FROM sku_abc
      ),
      sku_samples AS (
        SELECT dish_name, category_level1, received_amount, bill_count, abc_class
        FROM sku_abc
        WHERE received_amount < 1000 AND bill_count < 30
        ORDER BY received_amount ASC
        LIMIT 10
      )
      SELECT
        json_build_object(
          'items', json_build_array(
            json_build_object(
              'category', '食材成本差异回收',
              'baseline', (SELECT round((actual_cost - theoretical_cost)::numeric, 2) FROM cost_diff),
              'target_pct', 30,
              'opportunity', round((SELECT (actual_cost - theoretical_cost) FROM cost_diff) * 0.30, 2),
              'confidence', '中高',
              'owner', '商品/供应链/门店',
              'evidence', '采购价差、用量差、盘点差、报损差',
              'detail', (SELECT
                '实际食材成本' || (SELECT actual_cost FROM cost_diff) || '元 vs 理论成本' || (SELECT theoretical_cost FROM cost_diff) || '元，差异' || round((SELECT actual_cost - theoretical_cost FROM cost_diff)::numeric, 2) || '元（成本率' || round((SELECT actual_cost FROM cost_diff) / nullif((SELECT sum(received) FROM standard_stores), 0) * 100, 2) || '% vs 理论' || round((SELECT theoretical_cost FROM cost_diff) / nullif((SELECT sum(received) FROM standard_stores), 0) * 100, 2) || '%）。\n\n' ||
                '差异TOP5门店（需优先排查）：\n' ||
                string_agg(store_name || '：差异' || diff_amount || '元（实际成本率' || actual_cost_rate || '% vs 理论' || theoretical_cost_rate || '%，实收' || received || '元）', '；\n') ||
                '\n\n行动指向：\n1）上述5家门店实际成本率均远超理论值，需逐店排查采购单价与BOM标准价差异；\n2）盘点差——核查月末盘点与系统库存一致性，差异>3%需复盘；\n3）报损差——对比报损记录与行业基准，报损率>2%的门店需检查存储和加工流程；\n4）30天目标：将TOP5门店成本率降低5个百分点，预计回收' || round((SELECT (actual_cost - theoretical_cost) FROM cost_diff) * 0.30, 2) || '元。'
                FROM cost_diff_stores)
            ),
            json_build_object(
              'category', '标准店人工优化',
              'baseline', (SELECT round(total_wage / total_received * 100, 2) FROM labor),
              'target_pct', 100,
              'opportunity', round((SELECT total_wage * (1 - 24.0 / nullif(total_wage / total_received * 100, 0)) FROM labor), 2),
              'confidence', '中',
              'owner', '运营/人力',
              'evidence', '工时、工资、餐段销售与服务质量',
              'detail', (SELECT
                '标准店人工合计' || (SELECT total_wage FROM labor) || '元，费率' || round((SELECT total_wage FROM labor) / nullif((SELECT total_received FROM labor), 0) * 100, 2) || '%，目标降至24%。\n\n' ||
                '人工费率TOP5门店（需重点督导）：\n' ||
                string_agg(store_name || '：人工' || wage || '元（费率' || wage_rate || '%，实收' || received || '元）', '；\n') ||
                '\n\n行动指向：\n1）上述门店人工费率远超24%目标，需核查排班与实际打卡工时，识别冗余工时；\n2）低峰时段用小时工替代月薪员工，预计可降费率2-3个百分点；\n3）对费率>30%的门店启动人效专项督导，要求店长提交排班优化方案；\n4）30天目标：TOP5门店人工费率平均降低2个百分点。'
                FROM labor_stores)
            ),
            json_build_object(
              'category', '标准店能源优化',
              'baseline', (SELECT round(total_utility / total_received * 100, 2) FROM energy),
              'target_pct', 100,
              'opportunity', round((SELECT total_utility * (1 - 5.0 / nullif(total_utility / total_received * 100, 0)) FROM energy), 2),
              'confidence', '中',
              'owner', '工程/门店',
              'evidence', '账单/抄表、面积、营业时长',
              'detail', (SELECT
                '标准店水电合计' || (SELECT total_utility FROM energy) || '元，费率' || round((SELECT total_utility FROM energy) / nullif((SELECT total_received FROM energy), 0) * 100, 2) || '%，目标降至5%。\n\n' ||
                '水电费率TOP5门店（需排查设备）：\n' ||
                string_agg(store_name || '：水电' || utility || '元（费率' || utility_rate || '%，实收' || received || '元）', '；\n') ||
                '\n\n行动指向：\n1）上述门店水电费率远超5%目标，需排查是否存在设备老化、管道泄漏或空调空转；\n2）对比近3个月水电账单，波动>20%的门店需现场检查；\n3）缩短非营业时段的照明和空调，预计可降费率0.3-0.5个百分点；\n4）30天目标：TOP5门店水电费率平均降低0.5个百分点。'
                FROM energy_stores)
            ),
            json_build_object(
              'category', '高优惠门店治理',
              'baseline', (SELECT high_discount_stores FROM discount),
              'target_pct', 100,
              'opportunity', (SELECT theoretical_saving FROM discount),
              'confidence', '中低',
              'owner', '营销/运营',
              'evidence', '活动贡献、账单和复购不恶化',
              'detail', (SELECT
                '优惠率>25%的门店共' || (SELECT high_discount_stores FROM discount) || '家，理论可节约' || (SELECT theoretical_saving FROM discount) || '元/月。\n\n' ||
                '高优惠TOP5门店（需活动ROI评估）：\n' ||
                COALESCE(string_agg(store_name || '：优惠率' || discount_rate || '%（实收' || received || '元，理论可节约' || potential_saving || '元）', '；\n'), '无符合条件的门店') ||
                '\n\n行动指向：\n1）上述门店优惠率远超25%红线，需逐个活动对比优惠前后实收与客流变化；\n2）将优惠率从>25%降至20%以内，用会员积分/储值优惠替代直接打折；\n3）优惠率>20%的活动需总部审批，未审批的活动立即停用；\n4）30天目标：TOP5门店优惠率平均降低5个百分点。'
                FROM discount_stores)
            ),
            json_build_object(
              'category', '平台佣金优化',
              'baseline', (SELECT round(total_commission / nullif(platform_received, 0) * 100, 2) FROM platform),
              'target_pct', 100,
              'opportunity', GREATEST(round((SELECT total_commission * (1 - 20.0 / nullif(total_commission / nullif(platform_received, 0) * 100, 0)) FROM platform), 2), 0),
              'confidence', '中低',
              'owner', '外卖/采购',
              'evidence', '平台结算单与订单对账',
              'detail', (SELECT
                '平台佣金合计' || (SELECT total_commission FROM platform) || '元，佣金率' || round((SELECT total_commission FROM platform) / nullif((SELECT platform_received FROM platform), 0) * 100, 2) || '%。\n\n' ||
                '佣金费率TOP5门店（需对账核查）：\n' ||
                COALESCE(string_agg(store_code || '：佣金' || commission || '元（费率' || commission_rate || '%，实收' || received || '元）', '；\n'), '无数据') ||
                '\n\n行动指向：\n1）当前整体佣金率6.08%已低于20%目标，暂无大幅优化空间；\n2）上述门店佣金费率偏高，需逐月核对平台结算单与订单明细，识别多扣佣金；\n3）平台活动费与佣金应分离核算，避免活动费被计入佣金；\n4）提升自配送比例，降低对平台配送依赖；\n5）30天目标：完成TOP5门店平台结算单对账。'
                FROM platform_stores)
            ),
            json_build_object(
              'category', 'SKU复杂度压缩',
              'baseline', (SELECT low_sku_count FROM sku),
              'target_pct', 100,
              'opportunity', round((SELECT low_sku_revenue FROM sku) * 0.15, 2),
              'confidence', '低',
              'owner', '商品/运营',
              'evidence', '停用低效SKU减少独有原料库存、报损和采购复杂度',
              'detail', (SELECT
                '月收入<1000元且账单<30笔的低效SKU共' || (SELECT low_sku_count FROM sku) || '个，合计收入' || (SELECT low_sku_revenue FROM sku) || '元。\n\n' ||
                '低效SKU示例（候选停用）：\n' ||
                COALESCE(string_agg(dish_name || '（' || category_level1 || '）：收入' || received_amount || '元/' || bill_count || '笔，ABC类' || abc_class, '；\n'), '无数据') ||
                '\n\n行动指向：\n1）上述SKU月收入极低，停用后可减少独有原料库存和报损；\n2）逐个排查停用SKU的独有原料，计算可释放库存金额；\n3）每店SKU数减少15-20%，新品准入需通过收入预测和原料复用率审核；\n4）30天目标：完成' || (SELECT low_sku_count FROM sku) || '个低效SKU的停用评估。'
                FROM sku_samples)
            )
          )
        ) AS data
    `, [month])
    const rows = result.rows[0] as any
    sendSuccess(res, rows?.data)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 同比：当前月 vs 去年同月
router.get('/overview/yoy', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const prevYear = prevYearMonth(month)
    const [current, previous] = await Promise.all([
      query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]),
      query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [prevYear])
    ])
    const c = current.rows[0] as any
    const p = previous.rows[0] as any
    if (!c) { sendSuccess(res, null); return }
    const calcChange = (cur: number, prev: number | null) => {
      if (prev === null || prev === 0) return null
      return Math.round(((cur - prev) / Math.abs(prev)) * 100 * 100) / 100
    }
    sendSuccess(res, {
      current: c,
      previous: p,
      yoy: p ? {
        received_change_pct: calcChange(parseFloat(c.received), parseFloat(p.received)),
        bill_count_change_pct: calcChange(parseInt(c.bill_count), parseInt(p.bill_count)),
        avg_bill_value_change_pct: calcChange(parseFloat(c.avg_bill_value), parseFloat(p.avg_bill_value)),
        discount_rate_change: Math.round((parseFloat(c.discount_rate_pct) - parseFloat(p.discount_rate_pct)) * 100) / 100,
      } : null
    })
  } catch (err: any) { sendError(res, err.message) }
})

// 环比：当前月 vs 上月
router.get('/overview/mom', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const prevMo = prevMonth(month)
    const [current, previous] = await Promise.all([
      query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]),
      query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [prevMo])
    ])
    const c = current.rows[0] as any
    const p = previous.rows[0] as any
    if (!c) { sendSuccess(res, null); return }
    const calcChange = (cur: number, prev: number | null) => {
      if (prev === null || prev === 0) return null
      return Math.round(((cur - prev) / Math.abs(prev)) * 100 * 100) / 100
    }
    sendSuccess(res, {
      current: c,
      previous: p,
      mom: p ? {
        received_change_pct: calcChange(parseFloat(c.received), parseFloat(p.received)),
        bill_count_change_pct: calcChange(parseInt(c.bill_count), parseInt(p.bill_count)),
        avg_bill_value_change_pct: calcChange(parseFloat(c.avg_bill_value), parseFloat(p.avg_bill_value)),
        discount_rate_change: Math.round((parseFloat(c.discount_rate_pct) - parseFloat(p.discount_rate_pct)) * 100) / 100,
      } : null
    })
  } catch (err: any) { sendError(res, err.message) }
})

// 月度趋势
router.get('/overview/trend', async (req: AuthRequest, res) => {
  try {
    const endMonth = parseMonth(req)
    const months = parseInt(req.query.months as string) || 12
    const startDate = new Date(endMonth)
    startDate.setMonth(startDate.getMonth() - months + 1)
    const startMonth = startDate.toISOString().slice(0, 10)

    const result = await query(`
      SELECT month, bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct
      FROM analytics.mv_overview_monthly
      WHERE month >= $1::date AND month <= $2::date
      ORDER BY month
    `, [startMonth, endMonth])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 利润趋势（多月瀑布汇总）
router.get('/overview/profit-trend', async (req: AuthRequest, res) => {
  try {
    const endMonth = parseMonth(req)
    const months = parseInt(req.query.months as string) || 6
    const startDate = new Date(endMonth)
    startDate.setMonth(startDate.getMonth() - months + 1)
    const startMonth = startDate.toISOString().slice(0, 10)

    const result = await query(`
      WITH bill_monthly AS (
        SELECT date_trunc('month', closed_at)::date AS report_month,
          round(sum(discount_total)::numeric, 2) AS discount
        FROM analytics.bill_fact
        WHERE closed_at >= $1::date AND closed_at < ($2::date + interval '1 month')
        GROUP BY date_trunc('month', closed_at)
      ),
      monthly AS (
        SELECT e.report_month,
          round(sum(e.consumption)::numeric, 2) AS consumption,
          round(max(bm.discount)::numeric, 2) AS discount,
          round(sum(r.received)::numeric, 2) AS received,
          round(sum(e.actual_food_cost)::numeric, 2) AS food_cost,
          round(sum(e.wage_expense)::numeric, 2) AS wage,
          round(sum(e.rent_expense)::numeric, 2) AS rent,
          round(sum(e.utility_expense)::numeric, 2) AS utility,
          round(sum(e.operating_expense)::numeric, 2) AS total_expense,
          round(sum(r.received) - sum(e.actual_food_cost) - sum(e.operating_expense), 2) AS store_contribution
        FROM analytics.v_store_scorecard r
        JOIN analytics.mv_store_operating_expense_monthly e
          ON r.store_code = e.sales_store_code
        LEFT JOIN bill_monthly bm ON bm.report_month = e.report_month
        WHERE e.report_month >= $1::date AND e.report_month <= $2::date
          AND e.operating_expense IS NOT NULL AND r.received > 0
        GROUP BY e.report_month
        ORDER BY e.report_month
      )
      SELECT *,
        round(store_contribution / nullif(received, 0) * 100, 2) AS contribution_margin_pct
      FROM monthly
    `, [startMonth, endMonth])
    sendSuccess(res, result.rows)
  } catch (err: any) { sendError(res, err.message) }
})

// 时间段汇总
router.get('/overview/period', async (req: AuthRequest, res) => {
  try {
    const { start, end, mode, label } = parseDateRange(req)
    const result = await query(`
      SELECT
        sum(bill_count)::bigint AS bill_count,
        round(sum(received)::numeric, 2) AS received,
        round(sum(discounts)::numeric, 2) AS discounts,
        round(sum(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS discount_rate_pct,
        round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
        round(sum(guests)::numeric, 0) AS guests
      FROM analytics.v_store_daily
      WHERE business_date >= $1::date AND business_date < $2::date
    `, [start, end])
    sendSuccess(res, { ...result.rows[0], range_mode: mode, range_label: label, start_date: start, end_date: end })
  } catch (err: any) { sendError(res, err.message) }
})

// 渠道收入结构
router.get('/revenue/channel', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        business_date,
        round(cash::numeric, 2) AS cash,
        round(alipay::numeric, 2) AS alipay,
        round(wechat::numeric, 2) AS wechat,
        round(meituan::numeric, 2) AS meituan,
        round(unionpay::numeric, 2) AS unionpay,
        round(douyin::numeric, 2) AS douyin,
        round(credit::numeric, 2) AS credit,
        round(jd_delivery::numeric, 2) AS jd_delivery,
        round(meituan_delivery::numeric, 2) AS meituan_delivery,
        round(taobao_delivery::numeric, 2) AS taobao_delivery
      FROM analytics.v_channel_daily
      WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
      ORDER BY business_date
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 时段收入分布
router.get('/revenue/meal-period', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        meal_period,
        sum(bill_count)::bigint AS bill_count,
        round(sum(received)::numeric, 2) AS received,
        round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value
      FROM analytics.v_meal_period_daily
      WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
      GROUP BY meal_period
      ORDER BY sum(received) DESC
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 门店营收排名
router.get('/revenue/store-ranking', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        store_code,
        store_name,
        sum(bill_count)::bigint AS bill_count,
        round(sum(received)::numeric, 2) AS received,
        round(sum(discounts)::numeric, 2) AS discounts,
        round(sum(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS avg_discount_rate_pct,
        round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
        round(sum(guests)::numeric, 0) AS guests,
        round(avg(theoretical_margin_pct)::numeric, 2) AS avg_theoretical_margin_pct
      FROM analytics.v_store_daily
      WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
      GROUP BY store_code, store_name
      ORDER BY sum(received) DESC
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 日度营收汇总
router.get('/revenue/daily-summary', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const result = await query(`
      SELECT
        business_date,
        sum(bill_count)::bigint AS bill_count,
        round(sum(received + discounts)::numeric, 2) AS consumption,
        round(sum(received)::numeric, 2) AS received,
        round(sum(discounts)::numeric, 2) AS discounts,
        round(sum(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS discount_rate_pct,
        round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
        round(sum(guests)::numeric, 0) AS guests
      FROM analytics.v_store_daily
      WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
      GROUP BY business_date
      ORDER BY business_date
    `, [month])
    sendSuccess(res, result.rows)
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 银行授信报告
router.get('/bank/report', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const nextMonthDate = new Date(month)
    nextMonthDate.setMonth(nextMonthDate.getMonth() + 1)
    const nextMonth = nextMonthDate.toISOString().slice(0, 10)
    const [overview, daily, waterfall, risk, channel, storeRanking] = await Promise.all([
      query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]),
      query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = $1::date ORDER BY business_date`, [month]),
      query(`
        WITH bill_discount AS (
          SELECT store_code,
            round(sum(discount_total)::numeric, 2) AS discount
          FROM analytics.bill_fact
          WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
          GROUP BY store_code
        ),
        full_scope AS (
          SELECT r.received, e.consumption, COALESCE(bd.discount, e.consumption - e.received) AS discount,
                 e.actual_food_cost AS food_cost, e.wage_expense AS wage, e.rent_expense AS rent,
                 e.utility_expense AS utility, e.dorm_expense AS dorm, e.delivery_commission_expense AS commission,
                 e.card_fee_expense AS card_fee, e.repair_clean_expense AS repair, e.operating_expense AS operating_expense
          FROM analytics.mv_store_risk_rating_monthly r
          LEFT JOIN analytics.mv_store_operating_expense_monthly e ON r.store_code = e.sales_store_code AND e.report_month = $1::date
          LEFT JOIN bill_discount bd ON bd.store_code = r.store_code
          WHERE r.received IS NOT NULL AND e.operating_expense IS NOT NULL
        )
        SELECT round(sum(consumption)::numeric,2) AS consumption, round(sum(discount)::numeric,2) AS discount,
               round(sum(received)::numeric,2) AS received, round(sum(food_cost)::numeric,2) AS food_cost, round(sum(wage)::numeric,2) AS wage,
               round(sum(rent)::numeric,2) AS rent, round(sum(utility)::numeric,2) AS utility, round(sum(dorm)::numeric,2) AS dorm,
               round(sum(commission)::numeric,2) AS commission, round(sum(card_fee+repair)::numeric,2) AS other_expense,
               round(sum(operating_expense)::numeric,2) AS total_expense,
               round(sum(received)-sum(food_cost)-sum(operating_expense),2) AS store_contribution
        FROM full_scope
      `, [month]),
      query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`, [month]),
      query(`SELECT round(sum(cash)::numeric,2) AS cash, round(sum(alipay)::numeric,2) AS alipay, round(sum(wechat)::numeric,2) AS wechat, round(sum(meituan)::numeric,2) AS meituan, round(sum(unionpay)::numeric,2) AS unionpay, round(sum(douyin)::numeric,2) AS douyin, round(sum(credit)::numeric,2) AS credit, round(sum(jd_delivery)::numeric,2) AS jd_delivery, round(sum(meituan_delivery)::numeric,2) AS meituan_delivery, round(sum(taobao_delivery)::numeric,2) AS taobao_delivery FROM analytics.v_channel_daily WHERE business_date >= $1::date AND business_date < $2::date`, [month, nextMonth]),
      query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL ORDER BY received DESC`, [month]),
    ])

    const ov = overview.rows[0] as any
    const dailyRows = daily.rows.map((r: any) => ({
      ...r,
      received: parseFloat(r.received),
      bill_count: parseInt(r.bill_count),
      avg_bill_value: parseFloat(r.avg_bill_value),
      discount_rate_pct: parseFloat(r.discount_rate_pct),
    }))
    const wf = waterfall.rows[0] as any
    const riskRows = risk.rows.map((r: any) => ({
      ...r,
      store_count: parseInt(r.store_count),
      total_received: parseFloat(r.total_received),
    }))
    const ch = channel.rows[0] as any
    const channelTotals: Record<string, number> = {}
    if (ch) {
      for (const [k, v] of Object.entries(ch)) {
        const val = parseFloat(v as string)
        if (val > 0) channelTotals[k] = val
      }
    }
    const storeRows = storeRanking.rows.map((r: any) => ({
      ...r,
      received: parseFloat(r.received),
      bill_count: parseInt(r.bill_count),
      avg_bill_value: parseFloat(r.avg_bill_value),
      discount_rate_pct: parseFloat(r.discount_rate_pct),
      theoretical_margin_pct: parseFloat(r.theoretical_margin_pct),
    }))

    // 计算日度营收波动率
    const receivedArr = dailyRows.map((d: any) => d.received)
    const meanRev = receivedArr.reduce((a: number, b: number) => a + b, 0) / (receivedArr.length || 1)
    const variance = receivedArr.reduce((s: number, v: number) => s + Math.pow(v - meanRev, 2), 0) / (receivedArr.length || 1)
    const stdDev = Math.sqrt(variance)
    const cv = meanRev > 0 ? stdDev / meanRev : 0

    const f = (v: any) => v === null || v === undefined ? 0 : parseFloat(v)
    const i = (v: any) => v === null || v === undefined ? 0 : parseInt(v)
    sendSuccess(res, {
      overview: { ...ov, received: f(ov?.received), bill_count: i(ov?.bill_count), avg_bill_value: f(ov?.avg_bill_value), discount_rate_pct: f(ov?.discount_rate_pct), theoretical_margin_pct: f(ov?.theoretical_margin_pct), member_bills: i(ov?.member_bills), member_share_pct: f(ov?.member_share_pct) },
      daily: dailyRows,
      waterfall: { ...wf, consumption: f(wf?.consumption), discount: f(wf?.discount), received: f(wf?.received), food_cost: f(wf?.food_cost), wage: f(wf?.wage), rent: f(wf?.rent), utility: f(wf?.utility), dorm: f(wf?.dorm), commission: f(wf?.commission), other_expense: f(wf?.other_expense), total_expense: f(wf?.total_expense), store_contribution: f(wf?.store_contribution) },
      risk: riskRows,
      channel: channelTotals,
      stores: storeRows,
      stability: { mean_daily_revenue: meanRev, std_dev: stdDev, cv: cv, days: receivedArr.length },
    })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 中央厨房成本驾驶舱
router.get('/central-kitchen/dashboard', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const [summary, reconciliation, products, categoryCost, recipeEfficiency, mfgPool, yieldAnalysis] = await Promise.all([
      // 汇总指标
      query(`
        SELECT
          count(*) AS product_count,
          round(sum(inbound_quantity)::numeric,2) AS total_inbound_qty,
          round(sum(theoretical_cost)::numeric,2) AS theoretical_cost,
          round(sum(standard_cost)::numeric,2) AS standard_cost,
          round(sum(material_actual_cost)::numeric,2) AS material_actual_cost,
          round(max(manufacturing_cost_pool)::numeric,2) AS manufacturing_cost_pool,
          round(sum(allocated_manufacturing_cost)::numeric,2) AS allocated_manufacturing_cost,
          round(sum(full_manufacturing_cost)::numeric,2) AS full_manufacturing_cost,
          round(avg(NULLIF(full_unit_cost,0))::numeric,4) AS avg_unit_cost,
          round((sum(material_actual_cost) - sum(theoretical_cost))::numeric,2) AS efficiency_variance,
          round((sum(material_actual_cost) - sum(theoretical_cost)) / NULLIF(sum(theoretical_cost),0) * 100::numeric,2) AS efficiency_variance_pct,
          round((sum(material_actual_cost) - sum(standard_cost)) / NULLIF(sum(standard_cost),0) * 100::numeric,2) AS standard_variance_pct,
          round(max(manufacturing_cost_pool) / NULLIF(sum(material_actual_cost),0) * 100::numeric,2) AS mfg_cost_rate
        FROM analytics.v_central_kitchen_product_full_cost
        WHERE report_month = $1::date
      `, [month]),
      // 成本对账瀑布
      query(`
        SELECT
          count(*) AS product_lines,
          round(sum(p.theoretical_cost)::numeric,2) AS theoretical_cost,
          round(sum(p.standard_cost)::numeric,2) AS standard_cost,
          round(sum(p.material_actual_cost)::numeric,2) AS material_actual_cost,
          round(max(p.manufacturing_cost_pool)::numeric,2) AS manufacturing_cost_pool,
          round(sum(p.allocated_manufacturing_cost)::numeric,2) AS allocated_manufacturing_cost,
          round(sum(p.full_manufacturing_cost)::numeric,2) AS full_manufacturing_cost,
          round(sum(c.inbound_quantity * c.inbound_avg_unit_price)::numeric,2) AS calculated_inbound_value,
          round((sum(c.inbound_quantity * c.inbound_avg_unit_price) - sum(p.full_manufacturing_cost))::numeric,2) AS manufacturing_margin
        FROM analytics.v_central_kitchen_product_full_cost p
        JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
        WHERE p.report_month = $1::date
      `, [month]),
      // 产品明细列表
      query(`
        SELECT
          p.product_code,
          p.product_name,
          p.recipe_name,
          p.category_minor,
          p.unit,
          round(p.inbound_quantity::numeric,2) AS inbound_quantity,
          round(p.theoretical_cost::numeric,2) AS theoretical_cost,
          round(p.standard_cost::numeric,2) AS standard_cost,
          round(p.material_actual_cost::numeric,2) AS material_actual_cost,
          round(p.allocated_manufacturing_cost::numeric,2) AS allocated_manufacturing_cost,
          round(p.full_manufacturing_cost::numeric,2) AS full_manufacturing_cost,
          round(p.full_unit_cost::numeric,4) AS full_unit_cost,
          round(((p.material_actual_cost - p.theoretical_cost) / NULLIF(p.theoretical_cost,0) * 100)::numeric,2) AS efficiency_variance_pct,
          round(((p.material_actual_cost - p.standard_cost) / NULLIF(p.standard_cost,0) * 100)::numeric,2) AS standard_variance_pct,
          round((p.allocated_manufacturing_cost / NULLIF(p.material_actual_cost,0) * 100)::numeric,2) AS mfg_allocation_pct,
          round((c.inbound_quantity * c.inbound_avg_unit_price)::numeric,2) AS inbound_value,
          round((c.inbound_quantity * c.inbound_avg_unit_price - p.full_manufacturing_cost)::numeric,2) AS product_margin
        FROM analytics.v_central_kitchen_product_full_cost p
        JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
        WHERE p.report_month = $1::date
        ORDER BY p.full_manufacturing_cost DESC
      `, [month]),
      // 品类成本结构
      query(`
        SELECT
          COALESCE(NULLIF(category_minor,''),'未分类') AS category_minor,
          count(*) AS product_count,
          round(sum(inbound_quantity)::numeric,2) AS total_qty,
          round(sum(theoretical_cost)::numeric,2) AS theoretical_cost,
          round(sum(material_actual_cost)::numeric,2) AS material_actual_cost,
          round(sum(allocated_manufacturing_cost)::numeric,2) AS allocated_mfg_cost,
          round(sum(full_manufacturing_cost)::numeric,2) AS full_cost,
          round(avg(NULLIF(full_unit_cost,0))::numeric,4) AS avg_unit_cost,
          round(((sum(material_actual_cost) - sum(theoretical_cost)) / NULLIF(sum(theoretical_cost),0) * 100)::numeric,2) AS efficiency_var_pct
        FROM analytics.v_central_kitchen_product_full_cost
        WHERE report_month = $1::date
        GROUP BY category_minor
        ORDER BY full_cost DESC
      `, [month]),
      // 配方效率分析（Top超耗）
      query(`
        SELECT
          recipe_name,
          item_name,
          specification,
          unit,
          round(sum(theoretical_quantity)::numeric,2) AS theoretical_qty,
          round(sum(net_quantity)::numeric,2) AS actual_qty,
          round(sum(issue_quantity)::numeric,2) AS issue_qty,
          round(sum(theoretical_amount)::numeric,2) AS theoretical_amt,
          round(sum(issue_amount)::numeric,2) AS actual_amt,
          round(((sum(issue_amount) - sum(theoretical_amount)) / NULLIF(sum(theoretical_amount),0) * 100)::numeric,2) AS variance_pct,
          round(avg(actual_yield)::numeric,4) AS avg_actual_yield,
          round(avg(recipe_yield)::numeric,4) AS avg_recipe_yield,
          round((avg(actual_yield) - avg(recipe_yield))::numeric,4) AS yield_diff
        FROM central_kitchen_recipe_consumption
        WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
        GROUP BY recipe_name, item_name, specification, unit
        HAVING sum(theoretical_amount) > 0
        ORDER BY abs((sum(issue_amount) - sum(theoretical_amount)) / NULLIF(sum(theoretical_amount),0)) DESC
        LIMIT 20
      `, [month]),
      // 制造费用池明细
      query(`
        SELECT
          cost_type,
          cost_subtype,
          source_type,
          source_reference,
          round(source_amount::numeric,2) AS source_amount,
          round(central_kitchen_share_pct::numeric,4) AS share_pct,
          round(allocated_amount::numeric,2) AS allocated_amount,
          allocation_method,
          is_provisional,
          include_in_rebuilt_cost,
          note
        FROM central_kitchen_manufacturing_cost_pool
        WHERE report_month = $1::date
        ORDER BY include_in_rebuilt_cost DESC, allocated_amount DESC
      `, [month]),
      // 出成率分析
      query(`
        SELECT
          recipe_name,
          product_name,
          specification,
          unit,
          round(sum(theoretical_inbound_quantity)::numeric,2) AS theoretical_inbound_qty,
          round(sum(actual_inbound_quantity)::numeric,2) AS actual_inbound_qty,
          round(avg(achievement_rate)::numeric,4) AS avg_achievement_rate,
          round(avg(expected_quantity_variance_rate)::numeric,4) AS avg_expected_var_rate,
          round(sum(inbound_quantity)::numeric,2) AS total_inbound_qty,
          round(sum(return_quantity)::numeric,2) AS total_return_qty,
          round(sum(inbound_amount)::numeric,2) AS total_inbound_amt,
          round(sum(return_amount)::numeric,2) AS total_return_amt,
          round((sum(return_quantity) / NULLIF(sum(inbound_quantity),0) * 100)::numeric,2) AS return_rate
        FROM central_kitchen_finished_receipt
        WHERE receipt_date >= $1::date AND receipt_date < ($1::date + INTERVAL '1 month')
        GROUP BY recipe_name, product_name, specification, unit
        ORDER BY avg_expected_var_rate ASC
        LIMIT 20
      `, [month]),
    ])

    const s = summary.rows[0] as any
    const r = reconciliation.rows[0] as any

    const parseNum = (v: any) => v ? parseFloat(v) : 0
    const parseIntSafe = (v: any) => v ? parseInt(v) : 0

    sendSuccess(res, {
      summary: {
        product_count: parseIntSafe(s?.product_count),
        total_inbound_qty: parseNum(s?.total_inbound_qty),
        theoretical_cost: parseNum(s?.theoretical_cost),
        standard_cost: parseNum(s?.standard_cost),
        material_actual_cost: parseNum(s?.material_actual_cost),
        manufacturing_cost_pool: parseNum(s?.manufacturing_cost_pool),
        allocated_manufacturing_cost: parseNum(s?.allocated_manufacturing_cost),
        full_manufacturing_cost: parseNum(s?.full_manufacturing_cost),
        avg_unit_cost: parseNum(s?.avg_unit_cost),
        efficiency_variance: parseNum(s?.efficiency_variance),
        efficiency_variance_pct: parseNum(s?.efficiency_variance_pct),
        standard_variance_pct: parseNum(s?.standard_variance_pct),
        mfg_cost_rate: parseNum(s?.mfg_cost_rate),
      },
      reconciliation: {
        product_lines: parseIntSafe(r?.product_lines),
        theoretical_cost: parseNum(r?.theoretical_cost),
        standard_cost: parseNum(r?.standard_cost),
        material_actual_cost: parseNum(r?.material_actual_cost),
        manufacturing_cost_pool: parseNum(r?.manufacturing_cost_pool),
        allocated_manufacturing_cost: parseNum(r?.allocated_manufacturing_cost),
        full_manufacturing_cost: parseNum(r?.full_manufacturing_cost),
        calculated_inbound_value: parseNum(r?.calculated_inbound_value),
        manufacturing_margin: parseNum(r?.manufacturing_margin),
      },
      products: products.rows.map((row: any) => ({
        ...row,
        inbound_quantity: parseNum(row.inbound_quantity),
        theoretical_cost: parseNum(row.theoretical_cost),
        standard_cost: parseNum(row.standard_cost),
        material_actual_cost: parseNum(row.material_actual_cost),
        allocated_manufacturing_cost: parseNum(row.allocated_manufacturing_cost),
        full_manufacturing_cost: parseNum(row.full_manufacturing_cost),
        full_unit_cost: parseNum(row.full_unit_cost),
        efficiency_variance_pct: parseNum(row.efficiency_variance_pct),
        standard_variance_pct: parseNum(row.standard_variance_pct),
        mfg_allocation_pct: parseNum(row.mfg_allocation_pct),
        inbound_value: parseNum(row.inbound_value),
        product_margin: parseNum(row.product_margin),
      })),
      categoryCost: categoryCost.rows.map((row: any) => ({
        ...row,
        total_qty: parseNum(row.total_qty),
        theoretical_cost: parseNum(row.theoretical_cost),
        material_actual_cost: parseNum(row.material_actual_cost),
        allocated_mfg_cost: parseNum(row.allocated_mfg_cost),
        full_cost: parseNum(row.full_cost),
        avg_unit_cost: parseNum(row.avg_unit_cost),
        efficiency_var_pct: parseNum(row.efficiency_var_pct),
        product_count: parseIntSafe(row.product_count),
      })),
      recipeEfficiency: recipeEfficiency.rows.map((row: any) => ({
        ...row,
        theoretical_qty: parseNum(row.theoretical_qty),
        actual_qty: parseNum(row.actual_qty),
        issue_qty: parseNum(row.issue_qty),
        theoretical_amt: parseNum(row.theoretical_amt),
        actual_amt: parseNum(row.actual_amt),
        variance_pct: parseNum(row.variance_pct),
        avg_actual_yield: parseNum(row.avg_actual_yield),
        avg_recipe_yield: parseNum(row.avg_recipe_yield),
        yield_diff: parseNum(row.yield_diff),
      })),
      mfgPool: mfgPool.rows.map((row: any) => ({
        ...row,
        source_amount: parseNum(row.source_amount),
        share_pct: parseNum(row.share_pct),
        allocated_amount: parseNum(row.allocated_amount),
        is_provisional: row.is_provisional,
        include_in_rebuilt_cost: row.include_in_rebuilt_cost,
      })),
      yieldAnalysis: yieldAnalysis.rows.map((row: any) => ({
        ...row,
        theoretical_inbound_qty: parseNum(row.theoretical_inbound_qty),
        actual_inbound_qty: parseNum(row.actual_inbound_qty),
        avg_achievement_rate: parseNum(row.avg_achievement_rate),
        avg_expected_var_rate: parseNum(row.avg_expected_var_rate),
        total_inbound_qty: parseNum(row.total_inbound_qty),
        total_return_qty: parseNum(row.total_return_qty),
        total_inbound_amt: parseNum(row.total_inbound_amt),
        total_return_amt: parseNum(row.total_return_amt),
        return_rate: parseNum(row.return_rate),
      })),
    })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 配送—倒挤成本对账
router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)

    // 检查配送物化视图是否包含当前月份数据
    const distMvCheck = await query(`SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1`, [month])
    if (distMvCheck.rows.length === 0) {
      await query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
    }

    const [summary, storeReconciliation, topVariances, unmatchedItems, categoryReconciliation] = await Promise.all([
      // 汇总
      query(`
        WITH dist AS (
          SELECT d.store_code, d.item_code,
            round(sum(d.total_quantity)::numeric,2) AS dist_qty,
            round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
            round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax
          FROM mv_distribution_monthly d
          WHERE d.report_month = $1::date
          GROUP BY d.store_code, d.item_code
        ),
        inv AS (
          SELECT f.store_code, f.material_code,
            round(sum(f.opening_quantity)::numeric,2) AS opening_qty,
            round(sum(f.opening_amount)::numeric,2) AS opening_amt,
            round(sum(f.consumption_quantity)::numeric,2) AS consumption_qty,
            round(sum(f.consumption_amount)::numeric,2) AS consumption_amt,
            round(sum(f.ending_quantity)::numeric,2) AS ending_qty,
            round(sum(f.ending_amount)::numeric,2) AS ending_amt
          FROM analytics.fact_inventory_snapshot f
          WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
          GROUP BY f.store_code, f.material_code
        ),
        recon AS (
          SELECT COALESCE(d.store_code, i.store_code) AS store_code,
            COALESCE(d.item_code, i.material_code) AS item_code,
            COALESCE(d.dist_qty,0) AS dist_qty,
            COALESCE(d.dist_amt,0) AS dist_amt,
            COALESCE(d.dist_cost_excl_tax,0) AS dist_cost_excl_tax,
            COALESCE(i.opening_qty,0) AS opening_qty,
            COALESCE(i.opening_amt,0) AS opening_amt,
            COALESCE(i.consumption_qty,0) AS consumption_qty,
            COALESCE(i.consumption_amt,0) AS consumption_amt,
            COALESCE(i.ending_qty,0) AS ending_qty,
            COALESCE(i.ending_amt,0) AS ending_amt
          FROM dist d FULL OUTER JOIN inv i ON d.store_code = i.store_code AND d.item_code = i.material_code
        )
        SELECT
          count(*) AS total_lines,
          count(*) FILTER(WHERE dist_qty > 0 AND consumption_qty > 0) AS matched_lines,
          count(*) FILTER(WHERE dist_qty > 0 AND consumption_qty = 0) AS dist_only_lines,
          count(*) FILTER(WHERE dist_qty = 0 AND consumption_qty > 0) AS inv_only_lines,
          round(sum(dist_amt)::numeric,2) AS total_dist_amt,
          round(sum(dist_cost_excl_tax)::numeric,2) AS total_dist_cost_excl_tax,
          round(sum(opening_amt)::numeric,2) AS total_opening_amt,
          round(sum(consumption_amt)::numeric,2) AS total_consumption_amt,
          round(sum(ending_amt)::numeric,2) AS total_ending_amt,
          round((sum(opening_amt) + sum(dist_amt) - sum(ending_amt))::numeric,2) AS reverse_consumption_amt,
          round((sum(consumption_amt) - (sum(opening_amt) + sum(dist_amt) - sum(ending_amt)))::numeric,2) AS variance_amt,
          round((sum(consumption_amt) - (sum(opening_amt) + sum(dist_amt) - sum(ending_amt))) / NULLIF(sum(consumption_amt),0) * 100::numeric,2) AS variance_pct
        FROM recon
      `, [month]),
      // 门店维度对账
      query(`
        WITH dist AS (
          SELECT d.store_code,
            round(sum(d.total_quantity)::numeric,2) AS dist_qty,
            round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
            round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax
          FROM mv_distribution_monthly d
          WHERE d.report_month = $1::date
          GROUP BY d.store_code
        ),
        inv AS (
          SELECT f.store_code,
            round(sum(f.opening_amount)::numeric,2) AS opening_amt,
            round(sum(f.consumption_amount)::numeric,2) AS consumption_amt,
            round(sum(f.ending_amount)::numeric,2) AS ending_amt,
            count(*) FILTER(WHERE f.is_negative) AS neg_inventory_count
          FROM analytics.fact_inventory_snapshot f
          WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
          GROUP BY f.store_code
        ),
        store_names AS (
          SELECT DISTINCT store_code, store_name FROM mv_distribution_monthly
          WHERE report_month = $1::date
        )
        SELECT COALESCE(d.store_code, i.store_code) AS store_code,
          sn.store_name,
          COALESCE(d.dist_qty,0) AS dist_qty,
          COALESCE(d.dist_amt,0) AS dist_amt,
          COALESCE(d.dist_cost_excl_tax,0) AS dist_cost_excl_tax,
          COALESCE(i.opening_amt,0) AS opening_amt,
          COALESCE(i.consumption_amt,0) AS consumption_amt,
          COALESCE(i.ending_amt,0) AS ending_amt,
          round((COALESCE(i.opening_amt,0) + COALESCE(d.dist_amt,0) - COALESCE(i.ending_amt,0))::numeric,2) AS reverse_consumption_amt,
          round((COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + COALESCE(d.dist_amt,0) - COALESCE(i.ending_amt,0)))::numeric,2) AS variance_amt,
          round((COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + COALESCE(d.dist_amt,0) - COALESCE(i.ending_amt,0))) / NULLIF(COALESCE(i.consumption_amt,0),0) * 100::numeric,2) AS variance_pct,
          COALESCE(i.neg_inventory_count,0) AS neg_inventory_count
        FROM dist d FULL OUTER JOIN inv i ON d.store_code = i.store_code
        LEFT JOIN store_names sn ON COALESCE(d.store_code, i.store_code) = sn.store_code
        ORDER BY abs(COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + COALESCE(d.dist_amt,0) - COALESCE(i.ending_amt,0))) DESC
      `, [month]),
      // Top差异品项
      query(`
        WITH dist AS (
          SELECT d.store_code, d.item_code, d.item_name, d.unit, d.major_category, d.minor_category,
            round(sum(d.total_quantity)::numeric,2) AS dist_qty,
            round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt
          FROM mv_distribution_monthly d
          WHERE d.report_month = $1::date
          GROUP BY d.store_code, d.item_code, d.item_name, d.unit, d.major_category, d.minor_category
        ),
        inv AS (
          SELECT f.store_code, f.material_code,
            round(sum(f.opening_amount)::numeric,2) AS opening_amt,
            round(sum(f.consumption_amount)::numeric,2) AS consumption_amt,
            round(sum(f.ending_amount)::numeric,2) AS ending_amt
          FROM analytics.fact_inventory_snapshot f
          WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
          GROUP BY f.store_code, f.material_code
        )
        SELECT d.store_code, d.item_code, d.item_name, d.unit, d.major_category, d.minor_category,
          d.dist_qty, d.dist_amt,
          COALESCE(i.opening_amt,0) AS opening_amt,
          COALESCE(i.consumption_amt,0) AS consumption_amt,
          COALESCE(i.ending_amt,0) AS ending_amt,
          round((COALESCE(i.opening_amt,0) + d.dist_amt - COALESCE(i.ending_amt,0))::numeric,2) AS reverse_consumption_amt,
          round((COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + d.dist_amt - COALESCE(i.ending_amt,0)))::numeric,2) AS variance_amt,
          round((COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + d.dist_amt - COALESCE(i.ending_amt,0))) / NULLIF(COALESCE(i.consumption_amt,0),0) * 100::numeric,2) AS variance_pct
        FROM dist d LEFT JOIN inv i ON d.store_code = i.store_code AND d.item_code = i.material_code
        WHERE COALESCE(i.consumption_amt,0) > 0
        ORDER BY abs(COALESCE(i.consumption_amt,0) - (COALESCE(i.opening_amt,0) + d.dist_amt - COALESCE(i.ending_amt,0))) DESC
        LIMIT 30
      `, [month]),
      // 未匹配品项（有配送无库存耗用）
      query(`
        WITH dist_items AS (
          SELECT DISTINCT d.item_code, d.item_name, d.unit, d.major_category, d.minor_category,
            round(sum(d.total_quantity)::numeric,2) AS dist_qty,
            round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
            count(DISTINCT d.store_code) AS store_count
          FROM mv_distribution_monthly d
          WHERE d.report_month = $1::date
          GROUP BY d.item_code, d.item_name, d.unit, d.major_category, d.minor_category
        ),
        inv_items AS (
          SELECT DISTINCT material_code FROM analytics.fact_inventory_snapshot
          WHERE snapshot_date >= $1::date AND snapshot_date < ($1::date + INTERVAL '1 month')
        )
        SELECT d.item_code, d.item_name, d.unit, d.major_category, d.minor_category,
          d.dist_qty, d.dist_amt, d.store_count
        FROM dist_items d
        WHERE d.item_code NOT IN (SELECT material_code FROM inv_items)
        ORDER BY d.dist_amt DESC
        LIMIT 20
      `, [month]),
      // 品类维度对账
      query(`
        WITH dist AS (
          SELECT COALESCE(NULLIF(d.minor_category,''),'未分类') AS minor_category,
            round(sum(d.total_quantity)::numeric,2) AS dist_qty,
            round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
            round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax,
            count(DISTINCT d.item_code) AS item_count
          FROM mv_distribution_monthly d
          WHERE d.report_month = $1::date
          GROUP BY COALESCE(NULLIF(d.minor_category,''),'未分类')
        ),
        item_cat AS (
          SELECT DISTINCT item_code, COALESCE(NULLIF(minor_category,''),'未分类') AS minor_category
          FROM mv_distribution_monthly
          WHERE report_month = $1::date
        ),
        inv AS (
          SELECT ic.minor_category,
            round(sum(f.consumption_amount)::numeric,2) AS consumption_amt,
            round(sum(f.ending_amount)::numeric,2) AS ending_amt
          FROM analytics.fact_inventory_snapshot f
          JOIN item_cat ic ON f.material_code = ic.item_code
          WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
          GROUP BY ic.minor_category
        )
        SELECT COALESCE(d.minor_category, i.minor_category) AS minor_category,
          COALESCE(d.item_count,0) AS item_count,
          COALESCE(d.dist_qty,0) AS dist_qty,
          COALESCE(d.dist_amt,0) AS dist_amt,
          COALESCE(d.dist_cost_excl_tax,0) AS dist_cost_excl_tax,
          COALESCE(i.consumption_amt,0) AS consumption_amt,
          COALESCE(i.ending_amt,0) AS ending_amt,
          round((COALESCE(i.consumption_amt,0) - COALESCE(d.dist_amt,0) + COALESCE(i.ending_amt,0))::numeric,2) AS reverse_opening_amt,
          round((COALESCE(i.consumption_amt,0) - COALESCE(d.dist_amt,0))::numeric,2) AS variance_amt,
          round((COALESCE(i.consumption_amt,0) - COALESCE(d.dist_amt,0)) / NULLIF(COALESCE(d.dist_amt,0),0) * 100::numeric,2) AS variance_pct
        FROM dist d FULL OUTER JOIN inv i ON d.minor_category = i.minor_category
        ORDER BY COALESCE(d.dist_amt,0) DESC
      `, [month]),
    ])

    const s = summary.rows[0] as any
    const parseNum = (v: any) => v ? parseFloat(v) : 0
    const parseIntSafe = (v: any) => v ? parseInt(v) : 0

    sendSuccess(res, {
      summary: {
        total_lines: parseIntSafe(s?.total_lines),
        matched_lines: parseIntSafe(s?.matched_lines),
        dist_only_lines: parseIntSafe(s?.dist_only_lines),
        inv_only_lines: parseIntSafe(s?.inv_only_lines),
        total_dist_amt: parseNum(s?.total_dist_amt),
        total_dist_cost_excl_tax: parseNum(s?.total_dist_cost_excl_tax),
        total_opening_amt: parseNum(s?.total_opening_amt),
        total_consumption_amt: parseNum(s?.total_consumption_amt),
        total_ending_amt: parseNum(s?.total_ending_amt),
        reverse_consumption_amt: parseNum(s?.reverse_consumption_amt),
        variance_amt: parseNum(s?.variance_amt),
        variance_pct: parseNum(s?.variance_pct),
      },
      storeReconciliation: storeReconciliation.rows.map((row: any) => ({
        ...row,
        dist_qty: parseNum(row.dist_qty),
        dist_amt: parseNum(row.dist_amt),
        dist_cost_excl_tax: parseNum(row.dist_cost_excl_tax),
        opening_amt: parseNum(row.opening_amt),
        consumption_amt: parseNum(row.consumption_amt),
        ending_amt: parseNum(row.ending_amt),
        reverse_consumption_amt: parseNum(row.reverse_consumption_amt),
        variance_amt: parseNum(row.variance_amt),
        variance_pct: parseNum(row.variance_pct),
        neg_inventory_count: parseIntSafe(row.neg_inventory_count),
      })),
      topVariances: topVariances.rows.map((row: any) => ({
        ...row,
        dist_qty: parseNum(row.dist_qty),
        dist_amt: parseNum(row.dist_amt),
        opening_amt: parseNum(row.opening_amt),
        consumption_amt: parseNum(row.consumption_amt),
        ending_amt: parseNum(row.ending_amt),
        reverse_consumption_amt: parseNum(row.reverse_consumption_amt),
        variance_amt: parseNum(row.variance_amt),
        variance_pct: parseNum(row.variance_pct),
      })),
      unmatchedItems: unmatchedItems.rows.map((row: any) => ({
        ...row,
        dist_qty: parseNum(row.dist_qty),
        dist_amt: parseNum(row.dist_amt),
        store_count: parseIntSafe(row.store_count),
      })),
      categoryReconciliation: categoryReconciliation.rows.map((row: any) => ({
        ...row,
        item_count: parseIntSafe(row.item_count),
        dist_qty: parseNum(row.dist_qty),
        dist_amt: parseNum(row.dist_amt),
        dist_cost_excl_tax: parseNum(row.dist_cost_excl_tax),
        consumption_amt: parseNum(row.consumption_amt),
        ending_amt: parseNum(row.ending_amt),
        reverse_opening_amt: parseNum(row.reverse_opening_amt),
        variance_amt: parseNum(row.variance_amt),
        variance_pct: parseNum(row.variance_pct),
      })),
    })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 多级BOM成本穿透
router.get('/central-kitchen/bom-penetration', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const productCode = (req.query.productCode as string) || ''

    const [productList, bomTree, bomSummary, multiLevelChains] = await Promise.all([
      // 产品列表（含BOM层级数）
      query(`
        WITH bom_materials AS (
          SELECT DISTINCT rc.recipe_name, rc.item_code AS material_code
          FROM central_kitchen_recipe_consumption rc
          WHERE rc.business_date >= $1::date AND rc.business_date < ($1::date + INTERVAL '1 month')
        ),
        finished_products AS (
          SELECT product_code, product_name FROM central_kitchen_processing_cost
          WHERE report_month = $1::date
        ),
        multi_level AS (
          SELECT bm.material_code
          FROM bom_materials bm
          JOIN finished_products fp ON bm.material_code = fp.product_code
        )
        SELECT pc.product_code,
          pc.product_name,
          pc.category_minor,
          pc.unit,
          round(pc.inbound_quantity::numeric,2) AS inbound_quantity,
          round(pc.theoretical_cost::numeric,2) AS theoretical_cost,
          round(pc.actual_cost::numeric,2) AS actual_cost,
          round(pc.standard_cost::numeric,2) AS standard_cost,
          CASE WHEN pc.product_code IN (SELECT material_code FROM multi_level) THEN true ELSE false END AS has_multi_level_bom,
          (SELECT count(DISTINCT rc.item_code) FROM central_kitchen_recipe_consumption rc
           WHERE rc.business_date >= $1::date AND rc.business_date < ($1::date + INTERVAL '1 month')
             AND rc.recipe_name = pc.recipe_name) AS material_count
        FROM central_kitchen_processing_cost pc
        WHERE pc.report_month = $1::date
        ORDER BY pc.actual_cost DESC
      `, [month]),

      // BOM树：展开选中产品的配方
      query(`
        WITH RECURSIVE
        recipe_agg AS (
          SELECT rc.recipe_name, rc.item_code AS material_code, rc.item_name AS material_name,
            rc.unit,
            round(sum(rc.theoretical_quantity)::numeric,4) AS theoretical_qty,
            round(sum(rc.issue_quantity)::numeric,4) AS issue_qty,
            round(sum(rc.theoretical_amount)::numeric,2) AS theoretical_amt,
            round(sum(rc.issue_amount)::numeric,2) AS issue_amt,
            round(avg(rc.unit_price_excl_tax)::numeric,4) AS avg_unit_price
          FROM central_kitchen_recipe_consumption rc
          WHERE rc.business_date >= $1::date AND rc.business_date < ($1::date + INTERVAL '1 month')
          GROUP BY rc.recipe_name, rc.item_code, rc.item_name, rc.unit
        ),
        bom_tree AS (
          -- Level 1: 直接原材料
          SELECT
            pc.product_code AS root_product_code,
            pc.product_name AS root_product_name,
            pc.recipe_name AS root_recipe,
            1 AS level,
            ra.material_code,
            ra.material_name,
            ra.unit,
            ra.theoretical_qty,
            ra.issue_qty,
            ra.theoretical_amt,
            ra.issue_amt,
            ra.avg_unit_price,
            CASE WHEN pc2.product_code IS NOT NULL THEN true ELSE false END AS is_finished_product,
            ra.material_code AS path,
            '' AS parent_material_code
          FROM central_kitchen_processing_cost pc
          JOIN recipe_agg ra ON pc.recipe_name = ra.recipe_name
          LEFT JOIN central_kitchen_processing_cost pc2 ON ra.material_code = pc2.product_code AND pc2.report_month = $1::date
          WHERE pc.report_month = $1::date
            ${productCode ? 'AND pc.product_code = $2' : ''}

          UNION ALL

          -- Level 2+: 递归展开半成品
          SELECT
            bt.root_product_code,
            bt.root_product_name,
            bt.root_recipe,
            bt.level + 1,
            ra.material_code,
            ra.material_name,
            ra.unit,
            ra.theoretical_qty,
            ra.issue_qty,
            ra.theoretical_amt,
            ra.issue_amt,
            ra.avg_unit_price,
            CASE WHEN pc2.product_code IS NOT NULL THEN true ELSE false END AS is_finished_product,
            bt.path || ' -> ' || ra.material_code,
            bt.material_code AS parent_material_code
          FROM bom_tree bt
          JOIN central_kitchen_processing_cost pc ON bt.material_code = pc.product_code AND pc.report_month = $1::date
          JOIN recipe_agg ra ON pc.recipe_name = ra.recipe_name
          LEFT JOIN central_kitchen_processing_cost pc2 ON ra.material_code = pc2.product_code AND pc2.report_month = $1::date
          WHERE bt.is_finished_product AND bt.level < 5
            ${productCode ? '' : 'AND bt.level < 1'}
        )
        SELECT * FROM bom_tree
        ORDER BY root_product_code, level, theoretical_amt DESC
        ${productCode ? '' : 'LIMIT 200'}
      `, productCode ? [month, productCode] : [month]),

      // BOM汇总：每个产品的BOM成本结构
      query(`
        WITH bom AS (
          SELECT pc.product_code, pc.product_name,
            round(sum(rc.theoretical_amount)::numeric,2) AS bom_theoretical_amt,
            round(sum(rc.issue_amount)::numeric,2) AS bom_issue_amt,
            count(DISTINCT rc.item_code) AS material_count,
            count(DISTINCT rc.item_code) FILTER(WHERE pc2.product_code IS NOT NULL) AS sub_product_count,
            round(sum(rc.theoretical_amount) / NULLIF(pc.inbound_quantity, 0)::numeric,4) AS bom_unit_theoretical_cost,
            round(sum(rc.issue_amount) / NULLIF(pc.inbound_quantity, 0)::numeric,4) AS bom_unit_issue_cost
          FROM central_kitchen_processing_cost pc
          JOIN central_kitchen_recipe_consumption rc ON pc.recipe_name = rc.recipe_name
            AND rc.business_date >= $1::date AND rc.business_date < ($1::date + INTERVAL '1 month')
          LEFT JOIN central_kitchen_processing_cost pc2 ON rc.item_code = pc2.product_code AND pc2.report_month = $1::date
          WHERE pc.report_month = $1::date
          GROUP BY pc.product_code, pc.product_name, pc.inbound_quantity
        )
        SELECT b.*,
          round((b.bom_issue_amt - b.bom_theoretical_amt)::numeric,2) AS variance_amt,
          round((b.bom_issue_amt - b.bom_theoretical_amt) / NULLIF(b.bom_theoretical_amt, 0) * 100::numeric,2) AS variance_pct
        FROM bom b
        ORDER BY b.bom_theoretical_amt DESC
      `, [month]),

      // 多级BOM链：识别半成品依赖链
      query(`
        WITH bom_materials AS (
          SELECT DISTINCT rc.recipe_name, rc.item_code AS material_code, rc.item_name AS material_name
          FROM central_kitchen_recipe_consumption rc
          WHERE rc.business_date >= $1::date AND rc.business_date < ($1::date + INTERVAL '1 month')
        ),
        finished_products AS (
          SELECT product_code, product_name, recipe_name FROM central_kitchen_processing_cost
          WHERE report_month = $1::date
        )
        SELECT fp.product_code AS finished_code,
          fp.product_name AS finished_name,
          fp.recipe_name AS finished_recipe,
          bm.material_code AS sub_product_code,
          bm.material_name AS sub_product_name,
          (SELECT fp2.product_name FROM finished_products fp2 WHERE fp2.product_code = bm.material_code) AS sub_product_finished_name,
          (SELECT fp2.recipe_name FROM finished_products fp2 WHERE fp2.product_code = bm.material_code) AS sub_product_recipe
        FROM finished_products fp
        JOIN bom_materials bm ON fp.recipe_name = bm.recipe_name
        WHERE bm.material_code IN (SELECT product_code FROM finished_products)
        ORDER BY fp.product_name
      `, [month]),
    ])

    const parseNum = (v: any) => v ? parseFloat(v) : 0
    const parseIntSafe = (v: any) => v ? parseInt(v) : 0

    sendSuccess(res, {
      productList: productList.rows.map((row: any) => ({
        ...row,
        inbound_quantity: parseNum(row.inbound_quantity),
        theoretical_cost: parseNum(row.theoretical_cost),
        actual_cost: parseNum(row.actual_cost),
        standard_cost: parseNum(row.standard_cost),
        material_count: parseIntSafe(row.material_count),
        has_multi_level_bom: row.has_multi_level_bom,
      })),
      bomTree: bomTree.rows.map((row: any) => ({
        ...row,
        level: parseIntSafe(row.level),
        theoretical_qty: parseNum(row.theoretical_qty),
        issue_qty: parseNum(row.issue_qty),
        theoretical_amt: parseNum(row.theoretical_amt),
        issue_amt: parseNum(row.issue_amt),
        avg_unit_price: parseNum(row.avg_unit_price),
        is_finished_product: row.is_finished_product,
      })),
      bomSummary: bomSummary.rows.map((row: any) => ({
        ...row,
        bom_theoretical_amt: parseNum(row.bom_theoretical_amt),
        bom_issue_amt: parseNum(row.bom_issue_amt),
        material_count: parseIntSafe(row.material_count),
        sub_product_count: parseIntSafe(row.sub_product_count),
        bom_unit_theoretical_cost: parseNum(row.bom_unit_theoretical_cost),
        bom_unit_issue_cost: parseNum(row.bom_unit_issue_cost),
        variance_amt: parseNum(row.variance_amt),
        variance_pct: parseNum(row.variance_pct),
      })),
      multiLevelChains: multiLevelChains.rows.map((row: any) => ({
        ...row,
      })),
    })
  } catch (err: any) {
    sendError(res, err.message)
  }
})

// 销量驱动生产与要货计划
router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
  try {
    const month = parseMonth(req)
    const storeCode = (req.query.storeCode as string) || ''

    // 使用单一连接确保临时表在所有查询中可见
    const client = await pool.connect()

    try {
      // 检查物化视图是否包含当前月份数据，如缺失则刷新
      const mvCheck = await client.query(`
        SELECT 1 FROM mv_dish_sales_monthly WHERE month = $1::date LIMIT 1
      `, [month])
      if (mvCheck.rows.length === 0) {
        // 物化视图中没有该月数据，刷新物化视图
        await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dish_sales_monthly`)
      }

      // 检查配送物化视图是否包含当前月份数据
      const distMvCheck = await client.query(`
        SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1
      `, [month])
      if (distMvCheck.rows.length === 0) {
        await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
      }

      // 从物化视图创建临时表（索引扫描，毫秒级）
      await client.query(`
        CREATE TEMP TABLE IF NOT EXISTS tmp_sales_agg AS
        SELECT store_code, store_name, sku_code, dish_name, qty, amt, avg_unit_price
        FROM mv_dish_sales_monthly
        WHERE month = $1::date
        ${storeCode ? 'AND store_code = $2' : ''}
      `, storeCode ? [month, storeCode] : [month])

      await client.query(`CREATE INDEX IF NOT EXISTS idx_tmp_sales_sku ON tmp_sales_agg(sku_code)`)
      await client.query(`CREATE INDEX IF NOT EXISTS idx_tmp_sales_store ON tmp_sales_agg(store_code)`)

      // 总销量（全部从临时表获取，无需再扫描原表）
      const totalSales = await client.query(`
        SELECT
          count(DISTINCT dish_name) AS total_dish_count,
          round(sum(amt)::numeric,2) AS total_sales_amt
        FROM tmp_sales_agg
      `)

      const [summaryResult, skuSalesResult, materialDemandResult, storeDemandResult, ckProductionPlanResult, productionCoordResult] = await Promise.all([
        // 汇总（使用临时表）
        client.query(`
          WITH bom_skus AS (
            SELECT DISTINCT sku_code FROM analytics.fact_recipe_bom
          )
          SELECT
            count(*) AS total_sales_lines,
            count(DISTINCT s.dish_name) AS matched_sku_count,
            round(sum(s.qty)::numeric,2) AS matched_qty,
            round(sum(s.amt)::numeric,2) AS matched_amt,
            (SELECT count(DISTINCT material_code) FROM analytics.fact_recipe_bom) AS total_bom_materials
          FROM tmp_sales_agg s
          JOIN bom_skus bs ON s.sku_code = bs.sku_code
        `),

        // SKU销量明细（使用临时表）
        client.query(`
          WITH sales AS (
            SELECT dish_name,
              round(sum(qty)::numeric,2) AS qty,
              round(sum(amt)::numeric,2) AS amt,
              count(DISTINCT store_code) AS store_count,
              round(avg(avg_unit_price)::numeric,2) AS avg_unit_price
            FROM tmp_sales_agg
            GROUP BY dish_name
          )
          SELECT s.dish_name,
            sk.sku_code,
            s.qty,
            s.amt,
            s.store_count,
            s.avg_unit_price,
            CASE WHEN b.sku_code IS NOT NULL THEN true ELSE false END AS has_bom,
            COALESCE(b.material_count, 0) AS material_count
          FROM sales s
          LEFT JOIN analytics.dim_sku sk ON s.dish_name = sk.standard_name
          LEFT JOIN (
            SELECT sku_code, count(DISTINCT material_code) AS material_count
            FROM analytics.fact_recipe_bom
            GROUP BY sku_code
          ) b ON sk.sku_code = b.sku_code
          ORDER BY s.amt DESC
          LIMIT 50
        `),

        // 原料需求（使用临时表 × BOM展开，价格通过名称匹配配送记录）
        client.query(`
          WITH demand AS (
            SELECT s.store_code, b.material_code,
              m.material_name,
              b.unit,
              round(sum(s.qty * b.standard_gross_quantity)::numeric,4) AS demand_qty,
              round(sum(s.qty * b.standard_gross_quantity * COALESCE(ic.unit_price, 0))::numeric,2) AS demand_amt
            FROM tmp_sales_agg s
            JOIN analytics.fact_recipe_bom b ON s.sku_code = b.sku_code
            LEFT JOIN analytics.dim_material m ON b.material_code = m.material_code
            LEFT JOIN (
              SELECT d.item_name, round(sum(d.outbound_total_amount)::numeric / NULLIF(sum(d.total_quantity), 0), 4) AS unit_price
              FROM mv_distribution_monthly d
              WHERE d.report_month = $1::date
              GROUP BY d.item_name
            ) ic ON m.material_name = ic.item_name
            GROUP BY s.store_code, b.material_code, m.material_name, b.unit
          )
          SELECT
            material_code,
            material_name,
            unit,
            round(sum(demand_qty)::numeric,4) AS total_demand_qty,
            round(sum(demand_amt)::numeric,2) AS total_demand_amt,
            count(DISTINCT store_code) AS store_count,
            round(avg(demand_qty)::numeric,4) AS avg_store_demand
          FROM demand
          GROUP BY material_code, material_name, unit
          ORDER BY total_demand_amt DESC NULLS LAST
          LIMIT 100
        `, [month]),

        // 门店维度需求（使用临时表）
        client.query(`
          WITH store_demand AS (
            SELECT s.store_code, s.store_name,
              count(DISTINCT s.sku_code) AS sku_count,
              round(sum(s.qty)::numeric,2) AS total_qty,
              round(sum(s.amt)::numeric,2) AS total_amt,
              round(sum(s.qty * b.standard_gross_quantity)::numeric,4) AS total_material_demand_qty
            FROM tmp_sales_agg s
            LEFT JOIN analytics.fact_recipe_bom b ON s.sku_code = b.sku_code
            GROUP BY s.store_code, s.store_name
          )
          SELECT sd.store_code, sd.store_name,
            sd.sku_count,
            sd.total_qty,
            sd.total_amt,
            round(sd.total_material_demand_qty::numeric,4) AS total_material_demand_qty,
            COALESCE(inv.ending_qty, 0) AS ending_inventory_qty,
            COALESCE(inv.ending_amt, 0) AS ending_inventory_amt,
            round((sd.total_material_demand_qty - COALESCE(inv.ending_qty, 0))::numeric,4) AS suggested_order_qty
          FROM store_demand sd
          LEFT JOIN (
            SELECT store_code,
              round(sum(ending_quantity)::numeric,4) AS ending_qty,
              round(sum(ending_amount)::numeric,2) AS ending_amt
            FROM analytics.fact_inventory_snapshot
            WHERE snapshot_date >= $1::date AND snapshot_date < ($1::date + INTERVAL '1 month')
            GROUP BY store_code
          ) inv ON sd.store_code = inv.store_code
          ORDER BY sd.total_amt DESC
        `, [month]),

        // 中央厨房生产计划（使用临时表，通过名称匹配实际入库数据）
        client.query(`
          WITH ck_demand AS (
            SELECT b.material_code AS ck_product_code,
              m.material_name AS ck_product_name,
              b.unit,
              round(sum(s.qty * b.standard_gross_quantity)::numeric,4) AS demand_qty,
              count(DISTINCT s.store_code) AS store_count
            FROM tmp_sales_agg s
            JOIN analytics.fact_recipe_bom b ON s.sku_code = b.sku_code
            JOIN analytics.dim_material m ON b.material_code = m.material_code
            WHERE m.material_name IN (
              SELECT product_name FROM central_kitchen_processing_cost WHERE report_month = $1::date
            )
            GROUP BY b.material_code, m.material_name, b.unit
          ),
          ck_actual AS (
            SELECT product_code, product_name,
              round(sum(inbound_quantity)::numeric,4) AS actual_inbound_qty,
              round(sum(actual_cost)::numeric,2) AS actual_cost
            FROM central_kitchen_processing_cost
            WHERE report_month = $1::date
            GROUP BY product_code, product_name
          ),
          ck_combined AS (
            SELECT d.ck_product_code AS product_code,
              d.ck_product_name AS product_name,
              COALESCE(d.unit, '') AS unit,
              d.demand_qty,
              d.store_count,
              COALESCE(a.actual_inbound_qty, 0) AS actual_inbound_qty,
              COALESCE(a.actual_cost, 0) AS actual_cost
            FROM ck_demand d
            LEFT JOIN ck_actual a ON d.ck_product_name = a.product_name

            UNION ALL

            SELECT a.product_code, a.product_name, '',
              0, 0, a.actual_inbound_qty, a.actual_cost
            FROM ck_actual a
            WHERE a.product_name NOT IN (SELECT ck_product_name FROM ck_demand)
          )
          SELECT product_code, product_name, unit,
            demand_qty, store_count, actual_inbound_qty, actual_cost,
            round((demand_qty - actual_inbound_qty)::numeric,4) AS variance_qty,
            CASE WHEN actual_inbound_qty > 0
              THEN round((demand_qty - actual_inbound_qty) / actual_inbound_qty * 100::numeric, 2)
              ELSE NULL END AS variance_pct
          FROM ck_combined
          ORDER BY demand_qty DESC
        `, [month]),

        // 生产与配送协同
        client.query(`
          WITH ck_production AS (
            SELECT pc.product_code, pc.product_name,
              round(sum(pc.inbound_quantity)::numeric,4) AS inbound_qty,
              round(sum(pc.actual_cost)::numeric,2) AS actual_cost,
              round(sum(pc.theoretical_cost)::numeric,2) AS theoretical_cost
            FROM central_kitchen_processing_cost pc
            WHERE pc.report_month = $1::date
            GROUP BY pc.product_code, pc.product_name
          ),
          ck_distribution AS (
            SELECT d.item_code, d.item_name,
              round(sum(d.total_quantity)::numeric,4) AS dist_qty,
              round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt
            FROM mv_distribution_monthly d
            WHERE d.report_month = $1::date
              AND d.distribution_center_code = '2'
            GROUP BY d.item_code, d.item_name
          ),
          ck_inventory AS (
            SELECT f.material_code,
              round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
              round(sum(f.ending_quantity)::numeric,4) AS ending_qty
            FROM analytics.fact_inventory_snapshot f
            WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
              AND f.store_code = '2'
            GROUP BY f.material_code
          ),
          other_wh_inventory AS (
            SELECT f.material_code,
              round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
              round(sum(f.ending_quantity)::numeric,4) AS ending_qty
            FROM analytics.fact_inventory_snapshot f
            WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
              AND f.store_code IN (
                SELECT cost_unit_code FROM inventory_store_mapping
                WHERE sales_store_code IS NULL OR sales_store_code = ''
              )
              AND f.store_code != '2'
            GROUP BY f.material_code
          ),
          store_consumption AS (
            SELECT f.material_code,
              round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
              round(sum(f.consumption_amount)::numeric,2) AS consumption_amt,
              round(sum(f.ending_quantity)::numeric,4) AS ending_qty,
              round(sum(f.ending_amount)::numeric,2) AS ending_amt
            FROM analytics.fact_inventory_snapshot f
            WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
              AND f.store_code NOT IN (
                SELECT cost_unit_code FROM inventory_store_mapping
                WHERE sales_store_code IS NULL OR sales_store_code = ''
              )
            GROUP BY f.material_code
          )
          SELECT COALESCE(p.product_code, dist.item_code) AS product_code,
            COALESCE(p.product_name, dist.item_name) AS product_name,
            COALESCE(p.inbound_qty, 0) AS inbound_qty,
            COALESCE(p.actual_cost, 0) AS actual_cost,
            COALESCE(p.theoretical_cost, 0) AS theoretical_cost,
            COALESCE(dist.dist_qty, 0) AS dist_qty,
            COALESCE(dist.dist_amt, 0) AS dist_amt,
            COALESCE(ck.consumption_qty, 0) AS ck_consumption_qty,
            COALESCE(ck.ending_qty, 0) AS ck_ending_qty,
            COALESCE(owh.consumption_qty, 0) AS other_wh_consumption_qty,
            COALESCE(owh.ending_qty, 0) AS other_wh_ending_qty,
            COALESCE(sc.consumption_qty, 0) AS consumption_qty,
            COALESCE(sc.consumption_amt, 0) AS consumption_amt,
            COALESCE(sc.ending_qty, 0) AS ending_qty,
            COALESCE(sc.ending_amt, 0) AS ending_amt,
            round((COALESCE(dist.dist_qty, 0) - COALESCE(p.inbound_qty, 0))::numeric,4) AS production_distribution_gap,
            round((COALESCE(sc.consumption_qty, 0) - COALESCE(dist.dist_qty, 0))::numeric,4) AS distribution_consumption_gap,
            CASE WHEN COALESCE(p.inbound_qty, 0) > 0
              THEN round(COALESCE(dist.dist_qty, 0) / COALESCE(p.inbound_qty, 0) * 100::numeric, 2)
              ELSE NULL END AS completion_distribution_rate,
            CASE WHEN COALESCE(dist.dist_qty, 0) > 0
              THEN round(COALESCE(sc.consumption_qty, 0) / COALESCE(dist.dist_qty, 0) * 100::numeric, 2)
              ELSE NULL END AS distribution_consumption_rate,
            CASE WHEN COALESCE(sc.consumption_qty, 0) > 0
              THEN round(COALESCE(sc.ending_qty, 0) / COALESCE(sc.consumption_qty, 0) * 100::numeric, 2)
              ELSE NULL END AS inventory_accumulation_rate
          FROM ck_production p
          FULL OUTER JOIN ck_distribution dist ON p.product_code = dist.item_code
          FULL OUTER JOIN ck_inventory ck ON COALESCE(p.product_code, dist.item_code) = ck.material_code
          FULL OUTER JOIN other_wh_inventory owh ON COALESCE(p.product_code, dist.item_code) = owh.material_code
          FULL OUTER JOIN store_consumption sc ON COALESCE(p.product_code, dist.item_code) = sc.material_code
          ORDER BY COALESCE(p.actual_cost, 0) DESC
          LIMIT 50
        `, [month]),
      ])

      const parseNum = (v: any) => v ? parseFloat(v) : 0
      const parseIntSafe = (v: any) => v ? parseInt(v) : 0

      const s = summaryResult.rows[0] as any
      const ts = totalSales.rows[0] as any

      sendSuccess(res, {
        summary: {
          total_sales_lines: parseIntSafe(s?.total_sales_lines),
          matched_sku_count: parseIntSafe(s?.matched_sku_count),
          total_dish_count: parseIntSafe(ts?.total_dish_count),
          matched_qty: parseNum(s?.matched_qty),
          matched_amt: parseNum(s?.matched_amt),
          total_sales_amt: parseNum(ts?.total_sales_amt),
          total_bom_materials: parseIntSafe(s?.total_bom_materials),
          bom_coverage_pct: ts?.total_dish_count > 0 ? parseNum((s?.matched_sku_count / ts?.total_dish_count * 100).toFixed(2)) : 0,
        },
        skuSales: skuSalesResult.rows.map((row: any) => ({
          ...row,
          qty: parseNum(row.qty),
          amt: parseNum(row.amt),
          store_count: parseIntSafe(row.store_count),
          avg_unit_price: parseNum(row.avg_unit_price),
          material_count: parseIntSafe(row.material_count),
          has_bom: row.has_bom,
        })),
        materialDemand: materialDemandResult.rows.map((row: any) => ({
          ...row,
          total_demand_qty: parseNum(row.total_demand_qty),
          total_demand_amt: parseNum(row.total_demand_amt),
          store_count: parseIntSafe(row.store_count),
          avg_store_demand: parseNum(row.avg_store_demand),
        })),
        storeDemand: storeDemandResult.rows.map((row: any) => ({
          ...row,
          sku_count: parseIntSafe(row.sku_count),
          total_qty: parseNum(row.total_qty),
          total_amt: parseNum(row.total_amt),
          total_material_demand_qty: parseNum(row.total_material_demand_qty),
          ending_inventory_qty: parseNum(row.ending_inventory_qty),
          ending_inventory_amt: parseNum(row.ending_inventory_amt),
          suggested_order_qty: parseNum(row.suggested_order_qty),
        })),
        ckProductionPlan: ckProductionPlanResult.rows.map((row: any) => ({
          ...row,
          demand_qty: parseNum(row.demand_qty),
          actual_inbound_qty: parseNum(row.actual_inbound_qty),
          actual_cost: parseNum(row.actual_cost),
          variance_qty: parseNum(row.variance_qty),
          variance_pct: row.variance_pct ? parseNum(row.variance_pct) : null,
          store_count: parseIntSafe(row.store_count),
        })),
        productionCoord: productionCoordResult.rows.map((row: any) => ({
          ...row,
          inbound_qty: parseNum(row.inbound_qty),
          actual_cost: parseNum(row.actual_cost),
          theoretical_cost: parseNum(row.theoretical_cost),
          dist_qty: parseNum(row.dist_qty),
          dist_amt: parseNum(row.dist_amt),
          ck_consumption_qty: parseNum(row.ck_consumption_qty),
          ck_ending_qty: parseNum(row.ck_ending_qty),
          other_wh_consumption_qty: parseNum(row.other_wh_consumption_qty),
          other_wh_ending_qty: parseNum(row.other_wh_ending_qty),
          consumption_qty: parseNum(row.consumption_qty),
          consumption_amt: parseNum(row.consumption_amt),
          ending_qty: parseNum(row.ending_qty),
          ending_amt: parseNum(row.ending_amt),
          production_distribution_gap: parseNum(row.production_distribution_gap),
          distribution_consumption_gap: parseNum(row.distribution_consumption_gap),
          completion_distribution_rate: row.completion_distribution_rate ? parseNum(row.completion_distribution_rate) : null,
          distribution_consumption_rate: row.distribution_consumption_rate ? parseNum(row.distribution_consumption_rate) : null,
          inventory_accumulation_rate: row.inventory_accumulation_rate ? parseNum(row.inventory_accumulation_rate) : null,
        })),
      })
    } finally {
      client.release()
    }
  } catch (err: any) {
    sendError(res, err.message)
  }
})

export default router
