import { NextRequest, NextResponse } from 'next/server';
import { getUserFromRequest, jsonError } from '@/lib/auth-helpers';
import { getServiceRoleSupabase } from '@/lib/supabase';
import { rateLimitCheck, rateLimitKey, rateLimitResponse } from '@/middleware/rateLimit';

const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
const FREE_LIMIT = 5; // reads per calendar month
const monthKey = () => new Date().toISOString().slice(0, 7) + '-01';
const isSameMonth = (date: string | null | undefined) =>
  !!date && date.slice(0, 7) === new Date().toISOString().slice(0, 7);

const VALID_SPORTS = new Set(['NBA', 'MLB', 'WNBA', 'Soccer']);
const VALID_DIRECTIONS = new Set(['over', 'under']);
const VALID_NBA_STATS = new Set(['points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra', 'turnovers']);
const VALID_MLB_STATS = new Set([
  'strikeouts', 'hits_allowed', 'earned_runs', 'innings_pitched', 'walks_allowed',
  'hits', 'total_bases', 'rbi', 'runs', 'stolen_bases', 'home_runs', 'walks', 'singles', 'doubles',
]);
const VALID_SOCCER_STATS = new Set([
  'goals', 'assists', 'shots_on_target', 'shots', 'tackles',
  'cards', 'corners', 'saves', 'goals_conceded', 'passes', 'clean_sheet',
]);

export const dynamic = 'force-dynamic';

interface ScanBody {
  sport: 'NBA' | 'MLB' | 'WNBA' | 'Soccer';
  player: string;
  stat: string;
  line: number;
  direction: 'over' | 'under';
  book?: string;
}

export async function POST(req: NextRequest) {
  let body: ScanBody;
  try {
    body = (await req.json()) as ScanBody;
  } catch {
    return jsonError(400, 'Invalid JSON body.');
  }

  if (!VALID_SPORTS.has(body.sport)) return jsonError(400, 'Unknown sport.');
  if (!VALID_DIRECTIONS.has(body.direction)) return jsonError(400, 'Direction must be over or under.');
  if (typeof body.player !== 'string' || body.player.length === 0 || body.player.length > 80) {
    return jsonError(400, 'Player name is required.');
  }
  if (typeof body.line !== 'number' || !Number.isFinite(body.line) || body.line < 0 || body.line > 500) {
    return jsonError(400, 'Line must be a number between 0 and 500.');
  }

  const validStats =
    body.sport === 'MLB' ? VALID_MLB_STATS :
    body.sport === 'Soccer' ? VALID_SOCCER_STATS :
    VALID_NBA_STATS;
  if (!validStats.has(body.stat)) {
    return jsonError(400, `Stat "${body.stat}" not supported for ${body.sport}.`);
  }

  const user = await getUserFromRequest(req);
  const sb = getServiceRoleSupabase();

  // Per-minute rate limit (different limit per tier)
  const rl = rateLimitCheck(rateLimitKey(req), user?.tier ?? 'free');
  if (!rl.ok) return rateLimitResponse(rl.retryAfter);

  // Throttle free tier (monthly cap)
  if (user && user.tier === 'free' && sb) {
    const { data: profile } = await sb
      .from('user_profiles')
      .select('scan_count, scan_reset_date')
      .eq('id', user.id)
      .maybeSingle();

    const usedThisMonth = isSameMonth(profile?.scan_reset_date) ? (profile?.scan_count ?? 0) : 0;

    if (usedThisMonth >= FREE_LIMIT) {
      return NextResponse.json(
        {
          error: "You've used your 5 free reads this month. Unlock unlimited reads and full intelligence — Founder Access, $14.99/mo.",
          scans_remaining: 0,
          upgrade: { tier: 'analyst', price: 14.99 },
        },
        { status: 402 },
      );
    }
  }

  // Forward to backend grading engine
  try {
    const upstream = await fetch(`${BACKEND_URL}/api/analyze/prop`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(req.headers.get('authorization') ? { Authorization: req.headers.get('authorization')! } : {}),
      },
      body: JSON.stringify({
        sport: body.sport,
        player: body.player,
        stat_type: body.stat,
        line: body.line,
        direction: body.direction,
        book: body.book ?? 'draftkings',
      }),
    });

    const data = await upstream.json().catch(() => ({}));

    if (!upstream.ok) {
      return NextResponse.json(
        { error: data?.error || 'The engine hit a wall. Try that read again.' },
        { status: upstream.status },
      );
    }

    let scansRemaining: number | null = null;

    // Session 58 (work-order 1.5) — a refused read (no projection) writes
    // NOTHING: no scan_history, no ledger row. No hollow rows anywhere.
    const refused = data?.insufficient_data === true || !data?.grade;

    if (user && sb && !refused) {
      // Phase 1 — persist the read to the ledger (authenticated users only;
      // anonymous scans are never written: a null user_id row would pollute
      // the PUBLIC model record, which is pipeline-only). The line/book are
      // the REAL book values the slate pre-filled; locked odds are enriched
      // from the cached odds feed when the prop matches. Fire-and-forget —
      // the scan response never waits on the ledger.
      void writeLedgerEntry(sb, user.id, body, data);
    }

    if (user && sb && !refused) {
      void sb.rpc('increment_parlay_leg_frequency', {
        p_player: body.player,
        p_stat: body.stat,
        p_line: body.line,
        p_dir: body.direction,
        p_sport: body.sport,
        p_scan_delta: 1,
        p_parlay_delta: 0,
      });

      void sb.from('scan_history').insert({
        user_id: user.id,
        sport: body.sport,
        player_name: body.player,
        stat: body.stat,
        line: body.line,
        direction: body.direction,
        grade: data.grade,
        projection: data.projection,
        confidence: data.confidence,
        factors: data.factors ?? null,
      });

      if (user.tier === 'free') {
        const thisMonth = monthKey();
        const { data: current } = await sb
          .from('user_profiles')
          .select('scan_count, scan_reset_date')
          .eq('id', user.id)
          .maybeSingle();

        const next = (isSameMonth(current?.scan_reset_date) ? (current?.scan_count ?? 0) : 0) + 1;
        await sb
          .from('user_profiles')
          .update({ scan_count: next, scan_reset_date: thisMonth })
          .eq('id', user.id);
        scansRemaining = Math.max(0, FREE_LIMIT - next);
      }
    }

    // Session 67 — THE PRICE LAYER for the read card.
    //
    // A manual scan carries no market: the request is {player, stat, line,
    // direction}, so `analyzeViaEngine1` has no over/under prices to de-vig and
    // book_odds / fair_odds are legitimately absent from its response. The
    // prices DO exist for props the board graded, so we recover them from the
    // pre-graded snapshot — the same cache-only read (no odds fetch, no quota)
    // this route already performs for locked odds and team.
    //
    // The join is EXACT: player + stat + line + side. A user-chosen line the
    // board never graded has no market attached to it, so the triplet stays
    // hidden rather than borrowing a different line's price. `fair_odds` is
    // side-specific (over and under de-vig to different prices), which is why
    // side is part of the key and not an afterthought.
    const priceLayer = await lookupSnapshotPrices(body, req.headers.get('authorization')).catch(() => ({}));

    return NextResponse.json({
      ...data,
      ...priceLayer,
      scans_remaining: scansRemaining,
      tier: user?.tier ?? 'free',
    });
  } catch (err) {
    console.error('[scan] backend call failed', err);
    return jsonError(502, 'The engine hit a wall. Try that read again.');
  }
}

/**
 * Session 58 (Phase 1) — persist a completed user scan to ledger_entries.
 *
 * DATA SEMANTICS: `line`/`book` are the real book values the user scanned
 * (the slate pre-fills them from the odds feed). `locked_odds` attaches ONLY
 * when the cache-only snapshot carries the SAME line for this prop — odds
 * from a different line would be a fabrication, so absent beats wrong.
 * Upsert on the dedupe constraint: a double-tap never duplicates.
 */
/**
 * lookupSnapshotPrices — recover the market price layer for a manually-scanned
 * prop from the pre-graded snapshot.
 *
 * Returns `{}` when there is no exact match, so the card renders the
 * projection alone. NEVER returns a partial or borrowed price: if book and
 * fair aren't both present on the matched row, nothing is returned at all —
 * a lone book price is just the market repeated back, and a fair price
 * without its book has nothing to be fair against.
 *
 * `model_odds` / `ev_pct` arrive already tier-stripped by the backend for
 * unentitled callers (utils/snapshotGating), so the free tier's lock is
 * enforced upstream of this function, not here.
 */
async function lookupSnapshotPrices(body: ScanBody, authHeader?: string | null) {
  const { nameKey } = await import('@/lib/playerName');
  const sport = body.sport.toLowerCase();
  const playerKey = nameKey(body.player);
  const wantSide = body.direction === 'under' ? 'under' : 'over';

  const snap = await fetch(`${BACKEND_URL}/api/snapshot/${sport}`, {
    headers: { Accept: 'application/json', ...(authHeader ? { Authorization: authHeader } : {}) },
    cache: 'no-store',
  }).then((r) => (r.ok ? r.json() : null));

  interface SnapRow {
    player?: string; player_name?: string; stat_type?: string; stat?: string;
    line?: number; direction?: string; side?: string;
    book_odds?: number | null; fair_odds?: number | null; model_odds?: number | null;
    ev_pct?: number | null; quarantine_reason?: string | null; model_price_locked?: boolean;
  }
  const rows: SnapRow[] = snap?.grades || [];
  const match = rows.find((g) => {
    if (nameKey(g.player || g.player_name || '') !== playerKey) return false;
    if (String(g.stat_type || g.stat || '').toLowerCase() !== body.stat.toLowerCase()) return false;
    if (Number(g.line) !== Number(body.line)) return false;
    const side = String(g.direction || g.side || 'over').toLowerCase() === 'under' ? 'under' : 'over';
    return side === wantSide;
  });
  if (!match) return {};
  if (match.book_odds == null || match.fair_odds == null) return {};

  return {
    book_odds: match.book_odds,
    fair_odds: match.fair_odds,
    model_odds: match.model_odds ?? null,
    ev_pct: match.ev_pct ?? null,
    quarantine_reason: match.quarantine_reason ?? null,
    model_price_locked: match.model_price_locked === true,
  };
}

async function writeLedgerEntry(
  sb: NonNullable<ReturnType<typeof getServiceRoleSupabase>>,
  userId: string,
  body: ScanBody,
  data: { grade?: string; projection?: number; confidence?: number; edge_pct?: number },
) {
  try {
    const { nameKey } = await import('@/lib/playerName');
    const { buildManualLedgerRow, LEDGER_CONFLICT_COLS } = await import('@/lib/ledgerRow');
    const sport = body.sport.toLowerCase();
    const playerKey = nameKey(body.player);

    // Cache-only snapshot read (never triggers an odds fetch → no quota).
    let lockedOdds: string | null = null;
    let team: string | null = null;
    try {
      const snap = await fetch(`${BACKEND_URL}/api/snapshot/${sport}`, {
        headers: { Accept: 'application/json' },
        cache: 'no-store',
      }).then((r) => (r.ok ? r.json() : null));
      interface SnapGrade { player?: string; player_name?: string; stat_type?: string; stat?: string; team?: string | null; gradedAt?: { line?: number; odds?: number | string | null } }
      const grades: SnapGrade[] = snap?.grades || [];
      // Team match needs only player identity; odds additionally need the SAME line.
      const samePlayer = grades.filter(
        (g) => nameKey(g.player || g.player_name || '') === playerKey
          && String(g.stat_type || g.stat || '').toLowerCase() === body.stat.toLowerCase(),
      );
      team = samePlayer.find((g) => g.team)?.team ?? null;
      const match = samePlayer.find((g) => g.gradedAt && Number(g.gradedAt.line) === Number(body.line));
      if (match?.gradedAt?.odds != null) lockedOdds = String(match.gradedAt.odds);
    } catch { /* absent beats wrong */ }

    // Session 59 — team from the snapshot's stats resolve (real feed); opponent
    // stays null on manual scans (no game context — never guessed).
    const row = buildManualLedgerRow({
      userId,
      sport: body.sport,
      player: body.player,
      stat: body.stat,
      line: body.line,
      side: body.direction,
      book: body.book ?? 'draftkings',
      team,
      lockedOdds,
      grade: data.grade,
      edge: data.edge_pct,
      confidence: data.confidence,
      projection: data.projection,
    });

    await sb.from('ledger_entries').upsert(row, {
      onConflict: LEDGER_CONFLICT_COLS,
      ignoreDuplicates: true,
    });
  } catch (err) {
    console.warn('[scan] ledger write failed', err);
  }
}
