package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"context"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"
)

type adminHydraClientLister interface {
	ListClients(ctx context.Context, limit, offset int) ([]domain.HydraClient, error)
}

type identityCacheAdmin interface {
	GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
	FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error)
}

type AdminHandler struct {
	DB                 *gorm.DB
	Keto               service.KetoService
	KetoOutbox         repository.KetoOutboxRepository
	RPUsageQueries     domain.RPUsageQueryRepository
	TenantRepo         repository.TenantRepository
	Hydra              adminHydraClientLister
	AuditRepo          domain.AuditRepository
	UserProjectionRepo repository.UserProjectionRepository
	IdentityCache      identityCacheAdmin
	IntegrityChecker   repository.DataIntegrityChecker
}

const globalCustomClaimsSettingKey = "global_custom_claim_definitions"

type globalCustomClaimDefinition struct {
	Key             string `json:"key"`
	Label           string `json:"label"`
	ValueType       string `json:"valueType"`
	ReadPermission  string `json:"readPermission"`
	WritePermission string `json:"writePermission"`
	Description     string `json:"description,omitempty"`
}

type globalCustomClaimDefinitionsResponse struct {
	Items []globalCustomClaimDefinition `json:"items"`
}

func NewAdminHandler(keto service.KetoService, ketoOutbox repository.KetoOutboxRepository) *AdminHandler {
	return &AdminHandler{
		Keto:       keto,
		KetoOutbox: ketoOutbox,
	}
}

func (h *AdminHandler) GetRPUsageDaily(c *fiber.Ctx) error {
	if h == nil || h.RPUsageQueries == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
			"error": "rp usage query service unavailable",
		})
	}
	days := 14
	if raw := c.Query("days"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			days = parsed
		}
	}
	period := normalizeRPUsagePeriod(c.Query("period"))
	tenantID, allowed := h.authorizedRPUsageTenantID(c, strings.TrimSpace(c.Query("tenantId")))
	if !allowed {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
			"error": "forbidden: tenant rp usage stats permission denied",
		})
	}
	items, err := h.RPUsageQueries.FindRPUsage(c.Context(), domain.RPUsageQuery{
		Days:     days,
		Period:   period,
		TenantID: tenantID,
	})
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": err.Error(),
		})
	}
	return c.JSON(fiber.Map{
		"items":    items,
		"days":     days,
		"period":   period,
		"tenantId": tenantID,
	})
}

func normalizeRPUsagePeriod(period string) string {
	switch strings.ToLower(strings.TrimSpace(period)) {
	case "week":
		return "week"
	case "month":
		return "month"
	default:
		return "day"
	}
}

func (h *AdminHandler) authorizedRPUsageTenantID(c *fiber.Ctx, requestedTenantID string) (string, bool) {
	profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
	if profile != nil && domain.NormalizeRole(profile.Role) == domain.RoleSuperAdmin {
		return requestedTenantID, true
	}
	tenantID := requestedTenantID
	if tenantID == "" && profile != nil && profile.TenantID != nil {
		tenantID = strings.TrimSpace(*profile.TenantID)
	}
	if tenantID == "" {
		return "", false
	}
	if h == nil || h.Keto == nil || profile == nil || strings.TrimSpace(profile.ID) == "" {
		return "", false
	}
	allowed, err := h.Keto.CheckPermission(c.Context(), "User:"+profile.ID, "Tenant", tenantID, "view_rp_usage_stats")
	if err != nil || !allowed {
		return "", false
	}
	return tenantID, true
}

func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
}

func (h *AdminHandler) GetGlobalCustomClaimDefinitions(c *fiber.Ctx) error {
	if h == nil || h.DB == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
			"error": "settings store unavailable",
		})
	}

	var setting domain.SystemSetting
	if err := h.DB.WithContext(c.Context()).First(&setting, "key = ?", globalCustomClaimsSettingKey).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.JSON(globalCustomClaimDefinitionsResponse{Items: []globalCustomClaimDefinition{}})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}

	return c.JSON(globalCustomClaimDefinitionsResponse{
		Items: normalizeGlobalCustomClaimDefinitions(setting.Value["items"]),
	})
}

func (h *AdminHandler) UpdateGlobalCustomClaimDefinitions(c *fiber.Ctx) error {
	if h == nil || h.DB == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
			"error": "settings store unavailable",
		})
	}

	var req globalCustomClaimDefinitionsResponse
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	items, err := validateGlobalCustomClaimDefinitions(req.Items)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	setting := domain.SystemSetting{
		Key:   globalCustomClaimsSettingKey,
		Value: domain.JSONMap{"items": globalCustomClaimDefinitionsToJSON(items)},
	}
	if err := h.DB.WithContext(c.Context()).Save(&setting).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}

	return c.JSON(globalCustomClaimDefinitionsResponse{Items: items})
}

func normalizeGlobalCustomClaimDefinitions(value any) []globalCustomClaimDefinition {
	rawItems, ok := value.([]any)
	if !ok {
		return []globalCustomClaimDefinition{}
	}
	items := make([]globalCustomClaimDefinition, 0, len(rawItems))
	for _, item := range rawItems {
		raw, ok := item.(map[string]any)
		if !ok {
			continue
		}
		def := globalCustomClaimDefinition{
			Key:             strings.TrimSpace(stringValue(raw["key"])),
			Label:           strings.TrimSpace(stringValue(raw["label"])),
			ValueType:       normalizeGlobalCustomClaimType(stringValue(raw["valueType"])),
			ReadPermission:  adminNormalizeCustomClaimPermission(stringValue(raw["readPermission"])),
			WritePermission: adminNormalizeCustomClaimPermission(stringValue(raw["writePermission"])),
			Description:     strings.TrimSpace(stringValue(raw["description"])),
		}
		if def.Key != "" {
			items = append(items, def)
		}
	}
	return items
}

func validateGlobalCustomClaimDefinitions(items []globalCustomClaimDefinition) ([]globalCustomClaimDefinition, error) {
	seen := map[string]struct{}{}
	normalized := make([]globalCustomClaimDefinition, 0, len(items))
	for _, item := range items {
		key := strings.TrimSpace(item.Key)
		if key == "" {
			continue
		}
		if !isValidCustomClaimKey(key) {
			return nil, fiber.NewError(fiber.StatusBadRequest, "claim key must use letters, numbers, underscore, dot, or hyphen")
		}
		if _, exists := seen[key]; exists {
			return nil, fiber.NewError(fiber.StatusBadRequest, "duplicate claim key: "+key)
		}
		seen[key] = struct{}{}
		normalized = append(normalized, globalCustomClaimDefinition{
			Key:             key,
			Label:           strings.TrimSpace(item.Label),
			ValueType:       normalizeGlobalCustomClaimType(item.ValueType),
			ReadPermission:  adminNormalizeCustomClaimPermission(item.ReadPermission),
			WritePermission: adminNormalizeCustomClaimPermission(item.WritePermission),
			Description:     strings.TrimSpace(item.Description),
		})
	}
	return normalized, nil
}

func globalCustomClaimDefinitionsToJSON(items []globalCustomClaimDefinition) []any {
	values := make([]any, 0, len(items))
	for _, item := range items {
		values = append(values, map[string]any{
			"key":             item.Key,
			"label":           item.Label,
			"valueType":       item.ValueType,
			"readPermission":  item.ReadPermission,
			"writePermission": item.WritePermission,
			"description":     item.Description,
		})
	}
	return values
}

func normalizeGlobalCustomClaimType(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "number", "boolean", "array", "object", "date", "datetime":
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return "text"
	}
}

func adminNormalizeCustomClaimPermission(value string) string {
	if strings.TrimSpace(value) == "user_and_admin" {
		return "user_and_admin"
	}
	return "admin_only"
}

func isValidCustomClaimKey(value string) bool {
	for _, r := range value {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-' || r == '.' {
			continue
		}
		return false
	}
	return true
}

func stringValue(value any) string {
	if text, ok := value.(string); ok {
		return text
	}
	return ""
}

func requireSuperAdminProfile(c *fiber.Ctx) bool {
	profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
	if profile == nil || domain.NormalizeRole(profile.Role) != domain.RoleSuperAdmin {
		_ = c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: super_admin required"})
		return false
	}
	return true
}

func (h *AdminHandler) GetUserProjectionStatus(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.UserProjectionRepo == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "user projection service unavailable"})
	}
	status, err := h.UserProjectionRepo.GetStatus(c.Context())
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(status)
}

func (h *AdminHandler) GetOrySSOTSystemStatus(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.UserProjectionRepo == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "user projection service unavailable"})
	}
	projectionStatus, err := h.UserProjectionRepo.GetStatus(c.Context())
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}

	cacheStatus := domain.IdentityCacheStatus{
		Status:     "unavailable",
		RedisReady: false,
		LastError:  "identity cache service unavailable",
	}
	if h.IdentityCache != nil {
		cacheStatus, err = h.IdentityCache.GetIdentityCacheStatus(c.Context())
		if err != nil {
			return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
		}
	}

	return c.JSON(fiber.Map{
		"userProjection": projectionStatus,
		"identityCache":  cacheStatus,
	})
}

func (h *AdminHandler) FlushIdentityCache(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.IdentityCache == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity cache service unavailable"})
	}
	result, err := h.IdentityCache.FlushIdentityCache(c.Context())
	if err != nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(result)
}

func (h *AdminHandler) GetDataIntegrity(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.IntegrityChecker == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "data integrity checker unavailable"})
	}
	report, err := h.IntegrityChecker.CheckDataIntegrity(c.Context())
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(report)
}

func (h *AdminHandler) ListOrphanUserLoginIDs(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.IntegrityChecker == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "data integrity checker unavailable"})
	}
	items, err := h.IntegrityChecker.ListOrphanUserLoginIDs(c.Context())
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{
		"items": items,
		"total": len(items),
	})
}

func (h *AdminHandler) DeleteOrphanUserLoginIDs(c *fiber.Ctx) error {
	if !requireSuperAdminProfile(c) {
		return nil
	}
	if h == nil || h.IntegrityChecker == nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "data integrity checker unavailable"})
	}
	var req struct {
		IDs []string `json:"ids"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}
	result, err := h.IntegrityChecker.DeleteOrphanUserLoginIDs(c.Context(), req.IDs)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(result)
}

// GetSystemStats returns runtime statistics for monitoring
func (h *AdminHandler) GetSystemStats(c *fiber.Ctx) error {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	ctx := c.Context()

	stats := fiber.Map{
		"totalTenants":   h.countTenants(ctx),
		"totalUsers":     h.countUsers(ctx),
		"oidcClients":    h.countOIDCClients(ctx),
		"auditEvents24h": h.countAuditEventsSince(ctx, time.Now().UTC().Add(-24*time.Hour)),
		"goroutines":     runtime.NumGoroutine(),
		"cpus":           runtime.NumCPU(),
		"memory": fiber.Map{
			"alloc":      m.Alloc,
			"totalAlign": m.TotalAlloc,
			"sys":        m.Sys,
			"numGC":      m.NumGC,
		},
		"timestamp": time.Now(),
	}

	return c.Status(fiber.StatusOK).JSON(stats)
}

func (h *AdminHandler) countTenants(ctx context.Context) int64 {
	if h == nil || h.TenantRepo == nil {
		return 0
	}
	_, total, err := h.TenantRepo.List(ctx, 1, 0, "", "")
	if err != nil {
		return 0
	}
	return total
}

func (h *AdminHandler) countUsers(ctx context.Context) int64 {
	if h == nil || h.UserProjectionRepo == nil {
		return 0
	}
	status, err := h.UserProjectionRepo.GetStatus(ctx)
	if err != nil {
		return 0
	}
	return status.ProjectedUsers
}

func (h *AdminHandler) countOIDCClients(ctx context.Context) int64 {
	if h == nil || h.Hydra == nil {
		return 0
	}
	const pageSize = 500
	var total int64
	for offset := 0; ; offset += pageSize {
		clients, err := h.Hydra.ListClients(ctx, pageSize, offset)
		if err != nil {
			return total
		}
		for _, client := range clients {
			if isHiddenSystemClient(client) {
				continue
			}
			total++
		}
		if len(clients) < pageSize {
			break
		}
	}
	return total
}

func (h *AdminHandler) countAuditEventsSince(ctx context.Context, since time.Time) int64 {
	if h == nil || h.AuditRepo == nil {
		return 0
	}
	count, err := h.AuditRepo.CountEventsSince(ctx, since)
	if err == nil && count > 0 {
		return count
	}
	logs, pageErr := h.AuditRepo.FindPage(ctx, 10000, nil, "")
	if pageErr != nil {
		return count
	}
	var fallbackCount int64
	for _, log := range logs {
		if !log.Timestamp.Before(since) {
			fallbackCount++
		}
	}
	return fallbackCount
}
