import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import { getAdminSession } from '@/lib/auth'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

// Logged-in users queue control commands here (pause/resume a profile, mark a rung
// "await manual"). The mini drains them in its /api/trades-ingest response and applies
// them, so the dashboard can drive the bot without the mini exposing an inbound port.
const DIR = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads')
const STORE = path.join(DIR, 'trades-commands.json')
const ALLOWED = new Set(['pause', 'resume', 'await_manual', 'cancel_await', 'set_config', 'clear_config', 'reset_anchor', 'disable', 'enable', 'duplicate', 'create', 'rename', 'freeze', 'unfreeze', 'set_close', 'clear_close', 'handoff_close', 'cancel_handoff', 'reclaim_close', 'set_expiry', 'clear_expiry', 'manual_order', 'cancel_manual', 'edit_manual', 'fund_set_note'])
// Profile strategy values the dashboard may edit (mirror config.OVERRIDE_KEYS on the mini).
// max_price is the only nullable field (null => no ceiling); the rest must be positive numbers.
const CONFIG_KEYS = new Set(['tranche', 'tranche_scale_pct', 'first_step_pct', 'step_pct', 'profit_pct', 'max_rungs', 'max_price', 'min_price', 'stagger_tier'])

export async function POST(req: Request) {
  const session = await getAdminSession()
  if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
  let body: { symbol?: string; profile?: string; action?: string; price?: number; hours?: number; config?: unknown; cashBudget?: number; reverseInventory?: Record<string, unknown>; placeWithinPct?: number; staggerGap?: Record<string, unknown>; newProfile?: string; newSymbol?: string; autoDisable?: boolean; investor?: string; newName?: string; amount?: number; all?: boolean; note?: string; memo?: string; ts_txn?: number; expiry?: number; qty?: number; side?: string; id?: string; constrainedPlay?: unknown; constrainedCancel?: unknown; constrainedUntil?: unknown }
  try {
    body = await req.json()
  } catch {
    return NextResponse.json({ error: 'bad json' }, { status: 400 })
  }
  const action = typeof body.action === 'string' ? body.action : ''

  // Global (profile-less) settings: cash budget for longs + per-ticker share pools for shorts.
  if (action === 'set_settings') {
    const cashBudget = typeof body.cashBudget === 'number' && isFinite(body.cashBudget) && body.cashBudget > 0 ? body.cashBudget : undefined
    const reverseInventory: Record<string, number> = {}
    if (body.reverseInventory && typeof body.reverseInventory === 'object') {
      for (const [k, v] of Object.entries(body.reverseInventory)) {
        if (typeof v === 'number' && isFinite(v) && v >= 0) reverseInventory[k.toUpperCase()] = v
      }
    }
    const placeWithinPct = typeof body.placeWithinPct === 'number' && isFinite(body.placeWithinPct) && body.placeWithinPct > 0 && body.placeWithinPct <= 100 ? body.placeWithinPct : undefined
    const staggerGap: Record<string, number> = {}
    if (body.staggerGap && typeof body.staggerGap === 'object') {
      for (const [k, v] of Object.entries(body.staggerGap)) {
        if (typeof v === 'number' && isFinite(v) && v >= 0 && v <= 100) staggerGap[k.toUpperCase()] = v
      }
    }
    // Cash-constrained picker: which long profiles may claim cash for NEW buys while the budget
    // can't fund everyone. Each field travels only when sent; [] clears the picker, null clears
    // the expiry.
    const constrainedPlay = Array.isArray(body.constrainedPlay)
      ? (body.constrainedPlay as unknown[]).filter((k): k is string => typeof k === 'string')
          .map(k => k.toUpperCase().replace(/[^A-Z0-9_-]/g, '').slice(0, 24)).filter(Boolean).slice(0, 100)
      : undefined
    const constrainedCancel = typeof body.constrainedCancel === 'boolean' ? body.constrainedCancel : undefined
    const hasUntil = 'constrainedUntil' in body
    const constrainedUntil = typeof body.constrainedUntil === 'number' && isFinite(body.constrainedUntil) && body.constrainedUntil > 0
      ? body.constrainedUntil : null
    if (cashBudget === undefined && !Object.keys(reverseInventory).length && placeWithinPct === undefined && !Object.keys(staggerGap).length
        && constrainedPlay === undefined && constrainedCancel === undefined && !hasUntil) {
      return NextResponse.json({ error: 'cashBudget, reverseInventory, placeWithinPct, staggerGap or a picker field required' }, { status: 400 })
    }
    const cmd: Record<string, unknown> = { action: 'set_settings', cashBudget: cashBudget ?? null, reverseInventory, placeWithinPct: placeWithinPct ?? null, staggerGap, ts: Date.now() }
    if (constrainedPlay !== undefined) cmd.constrainedPlay = constrainedPlay
    if (constrainedCancel !== undefined) cmd.constrainedCancel = constrainedCancel
    if (hasUntil) cmd.constrainedUntil = constrainedUntil
    try {
      fs.mkdirSync(DIR, { recursive: true })
      let queue: unknown[] = []
      try { queue = JSON.parse(fs.readFileSync(STORE, 'utf8')) } catch { queue = [] }
      if (!Array.isArray(queue)) queue = []
      // MERGE into a still-pending set_settings rather than replacing it: a budget save followed by
      // a picker save inside one push window must both land (fields this save didn't send keep the
      // pending values).
      const prior = queue.find((c) => c && typeof c === 'object' && (c as { action?: string }).action === 'set_settings') as Record<string, unknown> | undefined
      if (prior) {
        for (const [k, v] of Object.entries(cmd)) {
          const empty = v === null || v === undefined || (typeof v === 'object' && !Array.isArray(v) && !Object.keys(v as object).length)
          if (!empty || (k === 'constrainedUntil') || (k === 'constrainedPlay')) prior[k] = v
          else if (!(k in prior)) prior[k] = v
        }
      } else {
        queue.push(cmd)
      }
      fs.writeFileSync(STORE, JSON.stringify(queue))
    } catch {
      return NextResponse.json({ error: 'store failed' }, { status: 500 })
    }
    return NextResponse.json({ ok: true, queued: cmd })
  }

  // Notional investor fund (profile-less): deposits/withdrawals/admin for the unitized ledger.
  if (action === 'fund_deposit' || action === 'fund_withdraw' || action === 'fund_set_base' || action === 'fund_remove_investor' || action === 'fund_rename_investor' || action === 'fund_set_note' || action === 'fund_txn_memo') {
    const investor = typeof body.investor === 'string' ? body.investor.trim().slice(0, 40) : ''
    // A note belongs to the investor and outlives individual transactions. Empty text clears it,
    // so this is deliberately NOT rejected for being blank.
    const note = typeof body.note === 'string' ? body.note.slice(0, 500) : ''
    // A memo belongs to ONE transaction — why this particular deposit or withdrawal happened.
    const memo = typeof body.memo === 'string' ? body.memo.slice(0, 200) : ''
    const amount = typeof body.amount === 'number' && isFinite(body.amount) && body.amount > 0 ? body.amount : undefined
    const all = body.all === true
    // Investors are keyed by name, so a rename is a key move on the bot side. It carries the
    // whole record across (txns, units, NAV history, memos, note) and mints/redeems nothing.
    const newName = typeof body.newName === 'string' ? body.newName.trim().slice(0, 40) : ''
    if (action === 'fund_rename_investor' && (!investor || !newName)) {
      return NextResponse.json({ error: 'investor + newName required' }, { status: 400 })
    }
    if (action === 'fund_rename_investor' && newName === investor) {
      return NextResponse.json({ error: 'new name is the same' }, { status: 400 })
    }
    if ((action === 'fund_deposit' || action === 'fund_remove_investor') && !investor) {
      return NextResponse.json({ error: 'investor required' }, { status: 400 })
    }
    if (action === 'fund_withdraw' && (!investor || (!all && amount === undefined))) {
      return NextResponse.json({ error: 'investor + amount (or all) required' }, { status: 400 })
    }
    if ((action === 'fund_deposit' || action === 'fund_set_base') && amount === undefined) {
      return NextResponse.json({ error: 'positive amount required' }, { status: 400 })
    }
    if (action === 'fund_set_note' && !investor) {
      return NextResponse.json({ error: 'investor required' }, { status: 400 })
    }
    // Editing an existing transaction's memo: identified by the transaction's own timestamp.
    // Only the memo is editable -- amount/price/units mint and redeem units, so changing them
    // would rewrite the fund's NAV history.
    const tsTxn = typeof body.ts_txn === 'number' && isFinite(body.ts_txn) ? body.ts_txn : undefined
    if (action === 'fund_txn_memo' && (!investor || tsTxn === undefined)) {
      return NextResponse.json({ error: 'investor + ts_txn required' }, { status: 400 })
    }
    const cmd = { action, investor, new_name: newName, amount: amount ?? null, all, note, memo, ts_txn: tsTxn ?? null, ts: Date.now() }
    try {
      fs.mkdirSync(DIR, { recursive: true })
      let queue: unknown[] = []
      try { queue = JSON.parse(fs.readFileSync(STORE, 'utf8')) } catch { queue = [] }
      if (!Array.isArray(queue)) queue = []
      queue.push(cmd)   // fund txns are cumulative — never de-duped
      fs.writeFileSync(STORE, JSON.stringify(queue))
    } catch {
      return NextResponse.json({ error: 'store failed' }, { status: 500 })
    }
    return NextResponse.json({ ok: true, queued: cmd })
  }

  // Commands address a PROFILE (a ticker may carry several). Fall back to symbol for back-compat.
  const profile = typeof body.profile === 'string' ? body.profile.toUpperCase()
    : typeof body.symbol === 'string' ? body.symbol.toUpperCase() : ''
  if (!profile || !ALLOWED.has(action)) {
    return NextResponse.json({ error: 'profile + valid action required' }, { status: 400 })
  }
  let config: Record<string, number | null> | null = null
  if (action === 'set_config') {
    const raw = body.config
    if (!raw || typeof raw !== 'object') {
      return NextResponse.json({ error: 'config object required' }, { status: 400 })
    }
    config = {}
    for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
      if (!CONFIG_KEYS.has(k)) continue
      if ((k === 'max_price' || k === 'min_price' || k === 'stagger_tier' || k === 'stagger_gap') && v === null) { config[k] = null; continue }
      if (typeof v !== 'number' || !isFinite(v) || v < 0) {
        return NextResponse.json({ error: `invalid value for ${k}` }, { status: 400 })
      }
      config[k] = v
    }
    if (!Object.keys(config).length) {
      return NextResponse.json({ error: 'no valid config fields' }, { status: 400 })
    }
  }
  // MANUAL LOT: your own order on a profile (limit price + qty). The bot places it (normal
  // session), folds it into that profile's ledger, and never lets it take a ladder rung. For
  // overnight you swap that order to EXTO in the broker app; the bot follows the replacement.
  let manualQty: number | null = null
  let manualSide: string | null = null
  let manualId: string | null = null
  if (action === 'manual_order') {
    const px = typeof body.price === 'number' && isFinite(body.price) ? body.price : NaN
    const q = typeof body.qty === 'number' && isFinite(body.qty) ? Math.round(body.qty) : NaN
    if (!(px > 0)) return NextResponse.json({ error: 'positive price required' }, { status: 400 })
    if (!(q >= 1)) return NextResponse.json({ error: 'qty of at least 1 required' }, { status: 400 })
    manualQty = q
    manualSide = body.side === 'SELL' ? 'SELL' : 'BUY'
  }
  if (action === 'cancel_manual' || action === 'edit_manual') {
    const id = typeof body.id === 'string' ? body.id.trim().slice(0, 40) : ''
    if (!id) return NextResponse.json({ error: 'manual lot id required' }, { status: 400 })
    if (action === 'edit_manual' && !(typeof body.price === 'number' && isFinite(body.price) && body.price > 0)) {
      return NextResponse.json({ error: 'positive price required' }, { status: 400 })
    }
    manualId = id
  }
  const needsNewKey = action === 'duplicate' || action === 'rename' || action === 'create'
  const newProfile = needsNewKey && typeof body.newProfile === 'string'
    ? body.newProfile.toUpperCase().replace(/[^A-Z0-9_-]/g, '').slice(0, 24) : null
  if (needsNewKey && !newProfile) {
    return NextResponse.json({ error: 'newProfile key required' }, { status: 400 })
  }
  // create carries the traded ticker for the brand-new profile (allow a dot, e.g. BRK.B)
  const newSymbol = action === 'create' && typeof body.newSymbol === 'string'
    ? body.newSymbol.toUpperCase().replace(/[^A-Z0-9.]/g, '').slice(0, 12) : null
  const cmd = {
    profile, symbol: profile, action, autoDisable: body.autoDisable === true,
    price: typeof body.price === 'number' ? body.price : null,
    hours: typeof body.hours === 'number' ? body.hours : null,
    expiry: typeof body.expiry === 'number' && isFinite(body.expiry) && body.expiry > 0 ? body.expiry : null,
    config, newProfile, newSymbol,
    qty: manualQty, side: manualSide, id: manualId,
    ts: Date.now(),
  }
  try {
    fs.mkdirSync(DIR, { recursive: true })
    let queue: unknown[] = []
    try { queue = JSON.parse(fs.readFileSync(STORE, 'utf8')) } catch { queue = [] }
    if (!Array.isArray(queue)) queue = []
    // de-dupe: one pending command per (profile, action) — latest wins (match old symbol-only items too).
    // set_config MERGES config fields instead: two quick edits to different fields (e.g. a stagger
    // step, then the ✎ form) must both survive — replacing would silently drop the earlier edit.
    // Manual-lot commands are DISCRETE events, never a latest-wins setting: two orders (or an
    // order and a cancel) queued inside one push window must both reach the bot, so they skip
    // the de-dupe entirely.
    const discrete = action === 'manual_order' || action === 'cancel_manual' || action === 'edit_manual'
    const prior = discrete ? undefined : queue.find((c) => c && typeof c === 'object'
      && ((c as { profile?: string }).profile ?? (c as { symbol?: string }).symbol) === profile
      && (c as { action?: string }).action === action)
    if (action === 'set_config' && prior && (prior as { config?: Record<string, number | null> }).config) {
      cmd.config = { ...(prior as { config: Record<string, number | null> }).config, ...(cmd.config || {}) }
    }
    queue = queue.filter((c) => c !== prior)
    queue.push(cmd)
    fs.writeFileSync(STORE, JSON.stringify(queue))
  } catch {
    return NextResponse.json({ error: 'store failed' }, { status: 500 })
  }
  return NextResponse.json({ ok: true, queued: cmd })
}
