package main

import (
	"crypto/sha256"
	"crypto/subtle"
	"net/http"
	"os"
	"time"

	"github.com/drjones/quantum-arcade/pkg/fees"
)

// Admin surfaces total operational visibility: every account, every
// transaction, treasury position, revenue, risk, and system health.
//
// It is deliberately read-only over game outcomes. The operator can see
// everything — including seeds, once revealed — but there is no control that
// changes a crash point or discloses a sealed seed mid-round. Such a control
// would make the fairness proof a lie, and the proof is the product. Money,
// accounts, withdrawals, and configuration are all operable.

// adminAuth gates the admin surface on a token supplied out of band.
//
// The comparison is constant-time and the token is never logged. If
// ARCADE_ADMIN_TOKEN is unset the admin surface is not mounted at all, so a
// default deployment has no admin endpoint to attack.
func adminAuth(next http.HandlerFunc) http.HandlerFunc {
	want := os.Getenv("ARCADE_ADMIN_TOKEN")
	wantHash := sha256.Sum256([]byte(want))

	return func(w http.ResponseWriter, r *http.Request) {
		got := bearer(r)
		if got == "" {
			got = r.URL.Query().Get("token") // convenience for the panel itself
		}
		gotHash := sha256.Sum256([]byte(got))
		if subtle.ConstantTimeCompare(wantHash[:], gotHash[:]) != 1 {
			// Do not distinguish "no token" from "wrong token".
			writeErr(w, http.StatusUnauthorized, "unauthorised")
			return
		}
		// The admin surface must never be cached by a proxy or a browser.
		w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
		w.Header().Set("X-Robots-Tag", "noindex, nofollow")
		next(w, r)
	}
}

func (s *server) routesAdmin(mux *http.ServeMux) {
	if os.Getenv("ARCADE_ADMIN_TOKEN") == "" {
		return // no token configured: no admin surface exists
	}
	mux.HandleFunc("GET /admin/api/overview", adminAuth(s.adminOverview))
	mux.HandleFunc("GET /admin/api/players", adminAuth(s.adminPlayers))
	mux.HandleFunc("GET /admin/api/transactions", adminAuth(s.adminTransactions))
	mux.HandleFunc("GET /admin/api/rounds", adminAuth(s.adminRounds))
	mux.HandleFunc("GET /admin/api/revenue", adminAuth(s.adminRevenue))
	mux.HandleFunc("GET /admin/api/risk", adminAuth(s.adminRisk))
}

// adminOverview is the headline position: what the house holds, what it owes,
// what it has earned, and whether the books balance.
func (s *server) adminOverview(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	house, err := s.ledger.AccountByName(ctx, "house_pot")
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	housePot, _ := s.ledger.Balance(ctx, house)
	issued, _ := s.ledger.TotalIssued(ctx)
	conservation, _ := s.ledger.ConservationCheck(ctx)

	var playerCount, activeToday int64
	_ = s.pool.QueryRow(ctx,
		`SELECT count(*) FROM accounts WHERE kind = 'player'`).Scan(&playerCount)
	_ = s.pool.QueryRow(ctx,
		`SELECT count(DISTINCT account_id) FROM postings
		  WHERE created_at > now() - interval '24 hours'`).Scan(&activeToday)

	var owedToPlayers int64
	_ = s.pool.QueryRow(ctx,
		`SELECT COALESCE(SUM(b.balance_msat), 0)
		   FROM account_balances b
		   JOIN accounts a ON a.id = b.account_id
		  WHERE a.kind = 'player'`).Scan(&owedToPlayers)

	// Fees collected, all time and today.
	var feesAllTime, feesToday int64
	_ = s.pool.QueryRow(ctx,
		`SELECT COALESCE(SUM(p.amount_msat), 0)
		   FROM postings p JOIN transactions t ON t.id = p.transaction_id
		  WHERE t.kind = 'operating_fee' AND p.account_id = $1`, house).Scan(&feesAllTime)
	_ = s.pool.QueryRow(ctx,
		`SELECT COALESCE(SUM(p.amount_msat), 0)
		   FROM postings p JOIN transactions t ON t.id = p.transaction_id
		  WHERE t.kind = 'operating_fee' AND p.account_id = $1
		    AND p.created_at > now() - interval '24 hours'`, house).Scan(&feesToday)

	var wagered24h, paid24h int64
	_ = s.pool.QueryRow(ctx,
		`SELECT COALESCE(SUM(stake_msat), 0) FROM bets
		  WHERE placed_at > now() - interval '24 hours'`).Scan(&wagered24h)
	_ = s.pool.QueryRow(ctx,
		`SELECT COALESCE(SUM(payout_msat), 0) FROM bets
		  WHERE settled_at > now() - interval '24 hours'`).Scan(&paid24h)

	members, _ := s.node.Members(ctx)

	writeJSON(w, http.StatusOK, map[string]any{
		"house_pot_msat":     housePot,
		"owed_to_players":    owedToPlayers,
		"total_issued_msat":  issued,
		"conservation_msat":  conservation,
		"books_balanced":     conservation == 0,
		"players_total":      playerCount,
		"players_active_24h": activeToday,
		"fees_all_time_msat": feesAllTime,
		"fees_24h_msat":      feesToday,
		"wagered_24h_msat":   wagered24h,
		"paid_out_24h_msat":  paid24h,
		"gross_margin_24h":   wagered24h - paid24h,
		"instances":          len(members),
		"generated_at":       time.Now().Format(time.RFC3339),
	})
}

// adminPlayers lists accounts with their position and activity.
func (s *server) adminPlayers(w http.ResponseWriter, r *http.Request) {
	rows, err := s.pool.Query(r.Context(), `
		SELECT a.id,
		       COALESCE(a.nickname, ''),
		       encode(a.pubkey, 'hex'),
		       a.created_at,
		       COALESCE(bal.balance_msat, 0),
		       COALESCE(st.bets, 0),
		       COALESCE(st.wagered, 0),
		       COALESCE(st.won, 0),
		       st.last_seen
		  FROM accounts a
		  LEFT JOIN account_balances bal ON bal.account_id = a.id
		  LEFT JOIN (
		      SELECT account_id,
		             count(*)                       AS bets,
		             COALESCE(SUM(stake_msat), 0)   AS wagered,
		             COALESCE(SUM(payout_msat), 0)  AS won,
		             max(placed_at)                 AS last_seen
		        FROM bets GROUP BY account_id
		  ) st ON st.account_id = a.id
		 WHERE a.kind = 'player'
		 ORDER BY COALESCE(bal.balance_msat, 0) DESC
		 LIMIT 500`)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	defer rows.Close()

	type player struct {
		ID          int64      `json:"id"`
		Nickname    string     `json:"nickname"`
		Pubkey      string     `json:"pubkey"`
		CreatedAt   time.Time  `json:"created_at"`
		BalanceMsat int64      `json:"balance_msat"`
		Bets        int64      `json:"bets"`
		WageredMsat int64      `json:"wagered_msat"`
		WonMsat     int64      `json:"won_msat"`
		NetMsat     int64      `json:"net_msat"`
		LastSeen    *time.Time `json:"last_seen"`
	}
	out := []player{}
	for rows.Next() {
		var p player
		if err := rows.Scan(&p.ID, &p.Nickname, &p.Pubkey, &p.CreatedAt,
			&p.BalanceMsat, &p.Bets, &p.WageredMsat, &p.WonMsat, &p.LastSeen); err != nil {
			writeErr(w, http.StatusInternalServerError, err.Error())
			return
		}
		p.NetMsat = p.WonMsat - p.WageredMsat
		out = append(out, p)
	}
	writeJSON(w, http.StatusOK, map[string]any{"players": out})
}

// adminTransactions is the raw ledger feed.
func (s *server) adminTransactions(w http.ResponseWriter, r *http.Request) {
	rows, err := s.pool.Query(r.Context(), `
		SELECT p.id, t.kind, t.round_id, p.account_id,
		       COALESCE(a.nickname, ''), p.amount_msat,
		       p.balance_before, p.balance_after, p.created_at
		  FROM postings p
		  JOIN transactions t ON t.id = p.transaction_id
		  JOIN accounts a ON a.id = p.account_id
		 ORDER BY p.id DESC
		 LIMIT 300`)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	defer rows.Close()

	type entry struct {
		ID            int64     `json:"id"`
		Kind          string    `json:"kind"`
		RoundID       *int64    `json:"round_id"`
		AccountID     int64     `json:"account_id"`
		Nickname      string    `json:"nickname"`
		AmountMsat    int64     `json:"amount_msat"`
		BalanceBefore int64     `json:"balance_before"`
		BalanceAfter  int64     `json:"balance_after"`
		CreatedAt     time.Time `json:"created_at"`
	}
	out := []entry{}
	for rows.Next() {
		var e entry
		if err := rows.Scan(&e.ID, &e.Kind, &e.RoundID, &e.AccountID, &e.Nickname,
			&e.AmountMsat, &e.BalanceBefore, &e.BalanceAfter, &e.CreatedAt); err != nil {
			writeErr(w, http.StatusInternalServerError, err.Error())
			return
		}
		out = append(out, e)
	}
	writeJSON(w, http.StatusOK, map[string]any{"transactions": out})
}

// adminRounds shows recent rounds with their economics and fairness record.
func (s *server) adminRounds(w http.ResponseWriter, r *http.Request) {
	rows, err := s.pool.Query(r.Context(), `
		SELECT r.id, r.game, r.crash_point, r.settled_at, r.voided_at,
		       encode(r.commitment, 'hex'),
		       CASE WHEN r.server_seed IS NULL THEN NULL
		            ELSE encode(r.server_seed, 'hex') END,
		       COALESCE(b.players, 0), COALESCE(b.staked, 0),
		       COALESCE(b.paid, 0), COALESCE(b.rake, 0)
		  FROM rounds r
		  LEFT JOIN (
		      SELECT round_id, count(*) AS players,
		             SUM(stake_msat) AS staked,
		             COALESCE(SUM(payout_msat), 0) AS paid,
		             COALESCE(SUM(rake_msat + rounding_msat), 0) AS rake
		        FROM bets GROUP BY round_id
		  ) b ON b.round_id = r.id
		 ORDER BY r.id DESC
		 LIMIT 100`)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	defer rows.Close()

	type round struct {
		ID         int64      `json:"id"`
		Game       string     `json:"game"`
		CrashPoint *int64     `json:"crash_point"`
		SettledAt  *time.Time `json:"settled_at"`
		VoidedAt   *time.Time `json:"voided_at"`
		Commitment string     `json:"commitment"`
		ServerSeed *string    `json:"server_seed"`
		Players    int64      `json:"players"`
		StakedMsat int64      `json:"staked_msat"`
		PaidMsat   int64      `json:"paid_msat"`
		RakeMsat   int64      `json:"rake_msat"`
		HouseMsat  int64      `json:"house_result_msat"`
	}
	out := []round{}
	for rows.Next() {
		var rd round
		if err := rows.Scan(&rd.ID, &rd.Game, &rd.CrashPoint, &rd.SettledAt, &rd.VoidedAt,
			&rd.Commitment, &rd.ServerSeed, &rd.Players, &rd.StakedMsat,
			&rd.PaidMsat, &rd.RakeMsat); err != nil {
			writeErr(w, http.StatusInternalServerError, err.Error())
			return
		}
		rd.HouseMsat = rd.StakedMsat - rd.PaidMsat
		out = append(out, rd)
	}
	writeJSON(w, http.StatusOK, map[string]any{"rounds": out})
}

// adminRevenue breaks earnings down by source and by day.
func (s *server) adminRevenue(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	house, err := s.ledger.AccountByName(ctx, "house_pot")
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}

	rows, err := s.pool.Query(ctx, `
		SELECT date_trunc('day', p.created_at) AS day,
		       COALESCE(SUM(p.amount_msat) FILTER (WHERE t.kind = 'bet'), 0),
		       COALESCE(SUM(-p.amount_msat) FILTER (WHERE t.kind = 'payout'), 0),
		       COALESCE(SUM(p.amount_msat) FILTER (WHERE t.kind = 'operating_fee'), 0),
		       COALESCE(SUM(p.amount_msat) FILTER (WHERE t.kind LIKE 'scratch%'), 0)
		  FROM postings p
		  JOIN transactions t ON t.id = p.transaction_id
		 WHERE p.account_id = $1
		   AND p.created_at > now() - interval '30 days'
		 GROUP BY 1 ORDER BY 1`, house)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	defer rows.Close()

	type day struct {
		Day      time.Time `json:"day"`
		StakesIn int64     `json:"stakes_in_msat"`
		PaidOut  int64     `json:"paid_out_msat"`
		Fees     int64     `json:"fees_msat"`
		Scratch  int64     `json:"scratch_net_msat"`
		NetMsat  int64     `json:"net_msat"`
	}
	out := []day{}
	for rows.Next() {
		var d day
		if err := rows.Scan(&d.Day, &d.StakesIn, &d.PaidOut, &d.Fees, &d.Scratch); err != nil {
			writeErr(w, http.StatusInternalServerError, err.Error())
			return
		}
		d.NetMsat = d.StakesIn - d.PaidOut + d.Fees
		out = append(out, d)
	}

	sch := fees.DefaultSchedule()
	writeJSON(w, http.StatusOK, map[string]any{
		"daily":        out,
		"fee_schedule": sch.Describe(9900),
	})
}

// adminRisk surfaces what an operator needs to notice: outsized winners,
// unresolved rounds, and pending withdrawals.
func (s *server) adminRisk(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	type winner struct {
		AccountID int64  `json:"account_id"`
		Nickname  string `json:"nickname"`
		NetMsat   int64  `json:"net_msat"`
		Bets      int64  `json:"bets"`
	}
	winners := []winner{}
	rows, err := s.pool.Query(ctx, `
		SELECT b.account_id, COALESCE(a.nickname, ''),
		       SUM(COALESCE(b.payout_msat, 0)) - SUM(b.stake_msat) AS net,
		       count(*)
		  FROM bets b JOIN accounts a ON a.id = b.account_id
		 GROUP BY b.account_id, a.nickname
		HAVING SUM(COALESCE(b.payout_msat, 0)) - SUM(b.stake_msat) > 0
		 ORDER BY net DESC LIMIT 20`)
	if err == nil {
		for rows.Next() {
			var v winner
			if err := rows.Scan(&v.AccountID, &v.Nickname, &v.NetMsat, &v.Bets); err == nil {
				winners = append(winners, v)
			}
		}
		rows.Close()
	}

	var unresolved, pendingWithdrawals, needsApproval int64
	_ = s.pool.QueryRow(ctx,
		`SELECT count(*) FROM rounds
		  WHERE settled_at IS NULL AND voided_at IS NULL
		    AND opened_at < now() - interval '2 minutes'`).Scan(&unresolved)
	_ = s.pool.QueryRow(ctx,
		`SELECT count(*) FROM lightning_withdrawals
		  WHERE status IN ('queued', 'sending')`).Scan(&pendingWithdrawals)
	_ = s.pool.QueryRow(ctx,
		`SELECT count(*) FROM lightning_withdrawals
		  WHERE status = 'needs_approval'`).Scan(&needsApproval)

	conservation, _ := s.ledger.ConservationCheck(ctx)

	writeJSON(w, http.StatusOK, map[string]any{
		"top_winners":           winners,
		"unresolved_rounds":     unresolved,
		"pending_withdrawals":   pendingWithdrawals,
		"withdrawals_to_review": needsApproval,
		"conservation_msat":     conservation,
		"books_balanced":        conservation == 0,
	})
}
