package httpapi

import (
	"context"
	"database/sql"
	"encoding/json"
	"net/http"
	"time"

	"github.com/wangjia/pangolin/server/internal/apierr"
	"github.com/wangjia/pangolin/server/internal/auth"
)

// AccountAPI serves /v1/me and supporting endpoints.
type AccountAPI struct {
	db *sql.DB
}

// NewAccountAPI creates an AccountAPI.
func NewAccountAPI(db *sql.DB) *AccountAPI { return &AccountAPI{db: db} }

// ─── GET /v1/me ──────────────────────────────────────────────────────────────

type meResponse struct {
	UUID   string `json:"uuid"`
	Email  string `json:"email"`
	DpUUID string `json:"dp_uuid"`
	Plan   string `json:"plan"` // "free" | "pro" | "team"
	// expire_at kept for the existing app; expires_at is the same value for the
	// web user-center (both RFC3339 UTC, null = no active sub).
	ExpireAt  *string `json:"expire_at"`
	ExpiresAt *string `json:"expires_at"`
	// Web user-center fields.
	DevicesUsed   int       `json:"devices_used"`
	DevicesMax    int       `json:"devices_max"`
	QuotaTodayMin *int      `json:"quota_today_min"` // null = unlimited (pro/team)
	DataTodayGB   float64   `json:"data_today_gb"`
	WeeklyGB      []float64 `json:"weekly_gb"` // last 7 days, oldest→newest
	TOTPEnabled   bool      `json:"totp_enabled"`
}

const bytesPerGB = 1024.0 * 1024.0 * 1024.0

// GetMe handles GET /v1/me.
func (a *AccountAPI) GetMe(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	uid, ok := auth.UserIDFromContext(ctx)
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}

	var (
		uuid        string
		email       string
		dpUUID      string
		totpEnabled bool
	)
	if err := a.db.QueryRowContext(ctx,
		`SELECT uuid, email, dp_uuid, totp_enabled FROM users WHERE id = ? AND status = 'active'`, uid,
	).Scan(&uuid, &email, &dpUUID, &totpEnabled); err == sql.ErrNoRows {
		apierr.WriteJSON(w, http.StatusNotFound, apierr.ErrNotFound)
		return
	} else if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	// Best active subscription → plan code + limits + expiry. Falls back to free.
	var (
		planCode     string
		maxDevices   int
		dailyMinutes sql.NullInt64
		expiresAt    sql.NullTime
	)
	err := a.db.QueryRowContext(ctx, `
		SELECT p.code, p.max_devices, p.daily_minutes, s.expires_at
		FROM subscriptions s
		JOIN plans p ON p.id = s.plan_id
		WHERE s.user_id = ? AND s.expires_at > ?
		ORDER BY s.expires_at DESC
		LIMIT 1
	`, uid, time.Now().UTC()).Scan(&planCode, &maxDevices, &dailyMinutes, &expiresAt)
	if err == sql.ErrNoRows {
		planCode = "free"
		if err := a.db.QueryRowContext(ctx,
			`SELECT max_devices, daily_minutes FROM plans WHERE code = 'free'`,
		).Scan(&maxDevices, &dailyMinutes); err != nil && err != sql.ErrNoRows {
			apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
			return
		}
	} else if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	var devicesUsed int
	_ = a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM devices WHERE user_id = ?`, uid).Scan(&devicesUsed)

	// Today's usage (UTC). Missing row → zero.
	// 日期在 Go 端算好传 ?,不用 MySQL 专属 UTC_DATE()(否则 SQLite 报错→恒 0)。
	var todayBytes uint64
	var todayMinutes int
	today := time.Now().UTC().Format("2006-01-02")
	_ = a.db.QueryRowContext(ctx, `
		SELECT COALESCE(bytes_up, 0) + COALESCE(bytes_down, 0), COALESCE(minutes_used, 0)
		FROM usage_daily WHERE user_id = ? AND date = ?
	`, uid, today).Scan(&todayBytes, &todayMinutes)

	resp := meResponse{
		UUID:        uuid,
		Email:       email,
		DpUUID:      dpUUID,
		Plan:        planCode,
		DevicesUsed: devicesUsed,
		DevicesMax:  maxDevices,
		DataTodayGB: float64(todayBytes) / bytesPerGB,
		WeeklyGB:    a.weeklyGB(ctx, uid),
		TOTPEnabled: totpEnabled,
	}
	if expiresAt.Valid {
		s := expiresAt.Time.UTC().Format(time.RFC3339)
		resp.ExpireAt = &s
		resp.ExpiresAt = &s
	}
	// quota_today_min: null when the plan is unlimited, else remaining minutes.
	if dailyMinutes.Valid {
		rem := int(dailyMinutes.Int64) - todayMinutes
		if rem < 0 {
			rem = 0
		}
		resp.QuotaTodayMin = &rem
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(resp)
}

// weeklyGB returns total GB per day for the last 7 UTC days (oldest→newest),
// filling days with no usage row as 0.
func (a *AccountAPI) weeklyGB(ctx context.Context, uid int64) []float64 {
	out := make([]float64, 7)
	// 起始日 Go 端算好传 ?(避免 MySQL 专属 UTC_DATE()/INTERVAL,SQLite 不支持)。
	weekFrom := time.Now().UTC().AddDate(0, 0, -6).Format("2006-01-02")
	rows, err := a.db.QueryContext(ctx, `
		SELECT date, COALESCE(bytes_up, 0) + COALESCE(bytes_down, 0)
		FROM usage_daily
		WHERE user_id = ? AND date >= ?
	`, uid, weekFrom)
	if err != nil {
		return out
	}
	defer rows.Close()

	byDay := make(map[string]uint64, 7)
	for rows.Next() {
		var d time.Time
		var b uint64
		if err := rows.Scan(&d, &b); err != nil {
			return out
		}
		byDay[d.UTC().Format("2006-01-02")] = b
	}
	now := time.Now().UTC()
	for i := 0; i < 7; i++ {
		day := now.AddDate(0, 0, -(6 - i)).Format("2006-01-02")
		out[i] = float64(byDay[day]) / bytesPerGB
	}
	return out
}

// ─── GET /v1/plans ───────────────────────────────────────────────────────────

type planResponse struct {
	Code         string `json:"code"`
	DailyMinutes *int64 `json:"daily_minutes"` // null = unlimited
	AdGate       bool   `json:"ad_gate"`
	PriceCents   int    `json:"price_cents"` // 价格(分);0 = 免费
	Currency     string `json:"currency"`    // CNY / USD …
	Period       string `json:"period"`      // month / year
}

// ListPlans handles GET /v1/plans.
// 套餐名为 UI 文案(客户端按 code 取 l10n),plans 表不含名称列,故不查 name_*。
func (a *AccountAPI) ListPlans(w http.ResponseWriter, r *http.Request) {
	rows, err := a.db.QueryContext(r.Context(),
		`SELECT code, daily_minutes, ad_gate, price_cents, currency, period FROM plans ORDER BY id`)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	defer rows.Close()

	var plans []planResponse
	for rows.Next() {
		var p planResponse
		var dm sql.NullInt64
		if err := rows.Scan(&p.Code, &dm, &p.AdGate, &p.PriceCents, &p.Currency, &p.Period); err != nil {
			apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
			return
		}
		if dm.Valid {
			p.DailyMinutes = &dm.Int64
		}
		plans = append(plans, p)
	}
	if err := rows.Err(); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]any{"plans": plans})
}

// ─── GET /v1/notices ─────────────────────────────────────────────────────────

// ListNotices handles GET /v1/notices. Returns an empty list for the MVP.
func (a *AccountAPI) ListNotices(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]any{"notices": []any{}})
}
