package service

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"strconv"
	"time"

	"oci-portal/internal/oci"
)

// 批式懒加载的单批条数上下限与缺省值。
const (
	auditLimitDefault = 100
	auditLimitMax     = 200
)

// 审计原始事件缓存:约 3KB/条,上限 2000 条 ≈ 6MB;TTL 内详情秒开。
const (
	auditRawMax = 2000
	auditRawTTL = 10 * time.Minute
)

// ErrInvalidAuditCursor 表示续查游标不可解析(过期格式/被篡改),handler 返回 400。
var ErrInvalidAuditCursor = errors.New("audit events: invalid cursor, refresh to restart")

// ErrAuditEventGone 表示原始事件缓存过期且小窗重查未找回;handler 映射 404。
var ErrAuditEventGone = errors.New("原始事件已不可取回,请刷新列表后重试")

// AuditQuery 是批式懒加载查询参数:Cursor 为空表示自当前时刻首查,
// 非空则从上次响应的游标位置继续向更早回溯;Limit 为单批目标条数;
// Q 为检索关键字,仅首查生效(续查沿用游标内嵌的关键字,保证跨批一致)。
type AuditQuery struct {
	Region string
	Cursor string
	Limit  int
	Q      string
}

// AuditEventsView 是批式查询响应:列表不含 raw(详情接口取回);
// Cursor 供下一批续查原样带回,空且 Exhausted 表示已到 365 天保留期尽头;
// ScannedThrough 为已完整回溯到的时刻(比它更新的时段已扫完),供前端展示进度。
type AuditEventsView struct {
	Items          []oci.AuditEvent `json:"items"`
	Cursor         string           `json:"cursor,omitempty"`
	Exhausted      bool             `json:"exhausted"`
	ScannedThrough *time.Time       `json:"scannedThrough,omitempty"`
}

// AuditEvents 实时查询租户 OCI 审计事件,纯透传不入库;region 为空时用配置
// 默认区域。原始事件剥离进进程内缓存,响应体瘦身约 10 倍(调研④方案 A1+B1)。
func (s *OciConfigService) AuditEvents(ctx context.Context, id uint, q AuditQuery) (AuditEventsView, error) {
	cur, err := decodeAuditCursor(q.Cursor)
	if err != nil {
		return AuditEventsView{}, err
	}
	if q.Cursor == "" {
		cur.Q = oci.SanitizeAuditTerm(q.Q)
	}
	cred, err := s.credentialsByID(ctx, id)
	if err != nil {
		return AuditEventsView{}, err
	}
	res, err := s.client.ListAuditEventsBatch(ctx, cred, q.Region, cur, auditLimit(q.Limit))
	if err != nil {
		return AuditEventsView{}, err
	}
	view := AuditEventsView{Items: s.stripAuditRaw(id, res.Items), Exhausted: res.Exhausted}
	if res.Cursor != nil {
		view.Cursor = encodeAuditCursor(*res.Cursor)
		view.ScannedThrough = &res.Cursor.End
	}
	return view, nil
}

// auditLimit 归一单批条数:缺省 100,上限 200(响应体量与页预算的平衡)。
func auditLimit(limit int) int {
	if limit <= 0 {
		return auditLimitDefault
	}
	return min(limit, auditLimitMax)
}

// encodeAuditCursor 把游标序列化为不透明字符串(base64url JSON)。
// 内容仅时间窗与 OCI 翻页令牌,伪造只影响自己的查询范围,无需签名。
func encodeAuditCursor(cur oci.AuditCursor) string {
	b, _ := json.Marshal(cur)
	return base64.RawURLEncoding.EncodeToString(b)
}

// decodeAuditCursor 解析续查游标;空串返回自当前时刻起的首查游标。
func decodeAuditCursor(s string) (oci.AuditCursor, error) {
	if s == "" {
		return oci.NewAuditCursor(time.Now()), nil
	}
	b, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil {
		return oci.AuditCursor{}, ErrInvalidAuditCursor
	}
	var cur oci.AuditCursor
	if err := json.Unmarshal(b, &cur); err != nil || cur.Start.IsZero() || !cur.Start.Before(cur.End) {
		return oci.AuditCursor{}, ErrInvalidAuditCursor
	}
	return cur, nil
}

// auditRawKey 组装租户隔离的原始事件缓存键。
func auditRawKey(configID uint, eventID string) string {
	return strconv.FormatUint(uint64(configID), 10) + ":" + eventID
}

// stripAuditRaw 把每条原始事件按租户与 eventId 放进缓存并从列表剥离。
func (s *OciConfigService) stripAuditRaw(configID uint, items []oci.AuditEvent) []oci.AuditEvent {
	for i := range items {
		if items[i].EventId != "" && items[i].Raw != nil {
			s.auditRaw.Set(auditRawKey(configID, items[i].EventId), items[i].Raw, auditRawTTL)
		}
		items[i].Raw = nil
	}
	return items
}

// InvalidateAuditCache 删除指定租户的全部审计原始事件缓存。
func (s *OciConfigService) InvalidateAuditCache(configID uint) {
	s.auditRaw.DeletePrefix(strconv.FormatUint(uint64(configID), 10) + ":")
}

// AuditEventDetail 取回单条事件的原始 JSON:缓存命中即回;miss 按事件时间
// 所在分钟起 2 分钟小窗重查兜底(调研④方案 A2),仍未命中报 ErrAuditEventGone。
func (s *OciConfigService) AuditEventDetail(ctx context.Context, id uint, region, eventID string, eventTime time.Time) (json.RawMessage, error) {
	if raw, ok := s.auditRaw.Get(auditRawKey(id, eventID)); ok {
		return raw.(json.RawMessage), nil
	}
	cred, err := s.credentialsByID(ctx, id)
	if err != nil {
		return nil, err
	}
	start := eventTime.UTC().Truncate(time.Minute)
	res, err := s.client.ListAuditEvents(ctx, cred, region, start, start.Add(2*time.Minute), "")
	if err != nil {
		return nil, err
	}
	var found json.RawMessage
	for _, ev := range res.Items {
		if ev.EventId == "" || ev.Raw == nil {
			continue
		}
		s.auditRaw.Set(auditRawKey(id, ev.EventId), ev.Raw, auditRawTTL)
		if ev.EventId == eventID {
			found = ev.Raw
		}
	}
	if found == nil {
		return nil, ErrAuditEventGone
	}
	return found, nil
}
