package agent

import (
	"context"
	"net/http"
	"slices"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/response"
)

// Usage window policy (plan §5.3): the default is the last 30 days and the
// query param days=7|30 selects the window; 30 is the hard cap. The window is
// always bounded well inside the 90-day audit retention (§3.5), so results
// are never silently truncated by the purge job.
const (
	defaultUsageWindowDays = 30
	maxUsageWindowDays     = 30
)

// usageStats is the aggregated result of the audit_logs usage queries.
type usageStats struct {
	dailyCounts       []APIKeyUsageDailyCount
	resourceBreakdown []APIKeyUsageResourceCount
	totalCalls        int64
	lastUsedAt        *time.Time
}

// usageStatsQuerier abstracts the audit_logs aggregation so tests can swap in
// a fake: the real implementation uses MySQL JSON syntax
// (metadata->>'$.agent_id') that sqlite cannot run.
type usageStatsQuerier interface {
	QueryUsageStats(ctx context.Context, keyID uint64, since time.Time) (*usageStats, error)
}

// gormUsageStatsQuerier is the production MySQL-backed implementation.
type gormUsageStatsQuerier struct {
	db *gorm.DB
}

func newGormUsageStatsQuerier(db *gorm.DB) *gormUsageStatsQuerier {
	return &gormUsageStatsQuerier{db: db}
}

// usageWhere matches audit rows produced by this API key. metadata->>'$.agent_id'
// is a JSON string extraction, so the numeric key id is cast to CHAR to align
// types (plan §5.3 constraint 2).
const usageWhere = `actor_type = 'agent' AND metadata->>'$.agent_id' = CAST(? AS CHAR) AND created_at >= ?`

func (q *gormUsageStatsQuerier) QueryUsageStats(ctx context.Context, keyID uint64, since time.Time) (*usageStats, error) {
	stats := &usageStats{}

	var daily []APIKeyUsageDailyCount
	if err := q.db.WithContext(ctx).Raw(
		// GROUP BY 必须用 SELECT 别名 date：MySQL ONLY_FULL_GROUP_BY 不认可
		// DATE_FORMAT(created_at,...) 与 GROUP BY DATE(created_at) 的函数依赖（Error 1055）。
		`SELECT DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(*) AS count
		 FROM audit_logs WHERE `+usageWhere+`
		 GROUP BY date ORDER BY date`,
		keyID, since,
	).Scan(&daily).Error; err != nil {
		return nil, err
	}
	stats.dailyCounts = daily

	var breakdown []APIKeyUsageResourceCount
	if err := q.db.WithContext(ctx).Raw(
		`SELECT resource_type, COUNT(*) AS count
		 FROM audit_logs WHERE `+usageWhere+`
		 GROUP BY resource_type ORDER BY count DESC`,
		keyID, since,
	).Scan(&breakdown).Error; err != nil {
		return nil, err
	}
	stats.resourceBreakdown = breakdown

	var aggregate struct {
		TotalCalls int64      `gorm:"column:total_calls"`
		LastUsedAt *time.Time `gorm:"column:last_used_at"`
	}
	if err := q.db.WithContext(ctx).Raw(
		`SELECT COUNT(*) AS total_calls, MAX(created_at) AS last_used_at
		 FROM audit_logs WHERE `+usageWhere,
		keyID, since,
	).Scan(&aggregate).Error; err != nil {
		return nil, err
	}
	stats.totalCalls = aggregate.TotalCalls
	stats.lastUsedAt = aggregate.LastUsedAt

	return stats, nil
}

// Usage handles GET /agent/api-keys/:id/usage (P5-T2, plan §5.3). Ownership
// scoping mirrors Get: regular users go through FindByIDForOwner, admins
// through FindByID; a missing key is 404 in both paths.
func (h *Handler) Usage(c *gin.Context) {
	claims := currentUser(c)
	if claims == nil {
		response.Error(c, http.StatusUnauthorized, "not authenticated")
		return
	}

	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid api key id")
		return
	}

	var key *model.APIKey
	if slices.Contains(claims.Roles, "admin") {
		key, err = h.repo.FindByID(c.Request.Context(), id)
	} else {
		key, err = h.repo.FindByIDForOwner(c.Request.Context(), id, claims.UserID)
	}
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to get api key")
		return
	}
	if key == nil {
		response.Error(c, http.StatusNotFound, "api key not found")
		return
	}

	days, valid := parseUsageWindowDays(c)
	if !valid {
		response.Error(c, http.StatusBadRequest, "invalid days: must be 7 or 30")
		return
	}

	if h.usage == nil {
		response.Error(c, http.StatusServiceUnavailable, "usage statistics unavailable")
		return
	}

	since := time.Now().AddDate(0, 0, -days)
	stats, err := h.usage.QueryUsageStats(c.Request.Context(), id, since)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to query usage stats")
		return
	}

	dailyCounts := stats.dailyCounts
	if dailyCounts == nil {
		dailyCounts = []APIKeyUsageDailyCount{}
	}
	breakdown := stats.resourceBreakdown
	if breakdown == nil {
		breakdown = []APIKeyUsageResourceCount{}
	}
	response.Success(c, &APIKeyUsage{
		DailyCounts:       dailyCounts,
		ResourceBreakdown: breakdown,
		TotalCalls:        stats.totalCalls,
		LastUsedAt:        stats.lastUsedAt,
		WindowDays:        days,
	})
}

// parseUsageWindowDays reads the optional days query param. Absent means the
// default 30-day window; when present only 7 or 30 are accepted (30 is the
// cap per plan §5.3).
func parseUsageWindowDays(c *gin.Context) (int, bool) {
	raw := c.Query("days")
	if raw == "" {
		return defaultUsageWindowDays, true
	}
	days, err := strconv.Atoi(raw)
	if err != nil || days <= 0 || days > maxUsageWindowDays || (days != 7 && days != 30) {
		return 0, false
	}
	return days, true
}
