package service

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"context"
	"fmt"
	"log/slog"
	"strings"
)

type RelyingPartyService interface {
	Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error)
	Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error)
	List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error)
	ListAll(ctx context.Context) ([]domain.RelyingParty, error)
	ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error)
	Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error)
	Delete(ctx context.Context, clientID string) error
}

type relyingPartyService struct {
	hydraService *HydraAdminService
	ketoService  KetoService
	outboxRepo   repository.KetoOutboxRepository
}

var defaultRelyingPartyOperatorRelations = []string{
	"admins",
	"creator",
	"config_editor",
	"secret_viewer",
	"secret_rotator",
	"jwks_viewer",
	"jwks_operator",
	"consent_viewer",
	"consent_revoker",
	"relationship_viewer",
	"audit_viewer",
	"status_operator",
}

func NewRelyingPartyService(
	hydraService *HydraAdminService,
	ketoService KetoService,
	outboxRepo repository.KetoOutboxRepository,
) RelyingPartyService {
	return &relyingPartyService{
		hydraService: hydraService,
		ketoService:  ketoService,
		outboxRepo:   outboxRepo,
	}
}

func extractRelyingPartyCreatorSubject(client *domain.HydraClient) string {
	if client == nil || client.Metadata == nil {
		return ""
	}
	raw, _ := client.Metadata["user_id"].(string)
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	return "User:" + raw
}

func (s *relyingPartyService) enqueueRelyingPartyTuple(ctx context.Context, action, object, relation, subject string) {
	if s.outboxRepo == nil || strings.TrimSpace(object) == "" || strings.TrimSpace(relation) == "" || strings.TrimSpace(subject) == "" {
		return
	}
	_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
		Namespace: "RelyingParty",
		Object:    object,
		Relation:  relation,
		Subject:   subject,
		Action:    action,
	})
}

func (s *relyingPartyService) enqueueDefaultRelyingPartyRelations(ctx context.Context, action string, client *domain.HydraClient, tenantID string) {
	if client == nil {
		return
	}

	tenantID = strings.TrimSpace(tenantID)
	if tenantID != "" {
		s.enqueueRelyingPartyTuple(ctx, action, client.ClientID, "parents", "Tenant:"+tenantID)
	}

	creatorSubject := extractRelyingPartyCreatorSubject(client)
	if creatorSubject == "" {
		return
	}

	for _, relation := range defaultRelyingPartyOperatorRelations {
		s.enqueueRelyingPartyTuple(ctx, action, client.ClientID, relation, creatorSubject)
	}
}

func (s *relyingPartyService) Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) {
	// 1. Create Client in Hydra
	if client.Metadata == nil {
		client.Metadata = make(map[string]any)
	}
	client.Metadata["tenant_id"] = tenantID

	createdClient, err := s.hydraService.CreateClient(ctx, client)
	if err != nil {
		return nil, fmt.Errorf("failed to create hydra client: %w", err)
	}

	// 2. Create default relations in Keto via Outbox.
	s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionCreate, createdClient, tenantID)

	return s.mapHydraToDomain(createdClient), nil
}

func (s *relyingPartyService) Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error) {
	hydraClient, err := s.hydraService.GetClient(ctx, clientID)
	if err != nil {
		return nil, nil, err
	}

	return s.mapHydraToDomain(hydraClient), hydraClient, nil
}

func (s *relyingPartyService) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) {
	// 1. Fetch ClientIDs from Keto
	// Relation tuple: RelyingParty:cid # parents @ Tenant:tid
	tuples, err := s.ketoService.ListRelations(ctx, "RelyingParty", "", "parents", "Tenant:"+tenantID)
	if err != nil {
		return nil, err
	}

	var rps []domain.RelyingParty
	for _, t := range tuples {
		clientID := t.Object
		client, err := s.hydraService.GetClient(ctx, clientID)
		if err != nil {
			slog.Warn("Failed to fetch relying party from hydra", "client_id", clientID, "error", err)
			continue
		}
		if rp := s.mapHydraToDomain(client); rp != nil {
			rps = append(rps, *rp)
		}
	}

	return rps, nil
}

func (s *relyingPartyService) ListAll(ctx context.Context) ([]domain.RelyingParty, error) {
	return nil, fmt.Errorf("ListAll not implemented in SSOT mode yet")
}

func (s *relyingPartyService) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) {
	var allRps []domain.RelyingParty
	for _, tid := range tenantIDs {
		rps, err := s.List(ctx, tid)
		if err == nil {
			allRps = append(allRps, rps...)
		}
	}
	return allRps, nil
}

func (s *relyingPartyService) Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error) {
	updatedClient, err := s.hydraService.UpdateClient(ctx, clientID, client)
	if err != nil {
		return nil, err
	}
	return s.mapHydraToDomain(updatedClient), nil
}

func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error {
	// 1. Get client to find tenantID (for Keto cleanup)
	client, err := s.hydraService.GetClient(ctx, clientID)
	if err != nil {
		return err
	}
	tenantID := ""
	if client.Metadata != nil {
		if tid, ok := client.Metadata["tenant_id"].(string); ok {
			tenantID = tid
		}
	}

	// 2. Delete from Hydra
	if err := s.hydraService.DeleteClient(ctx, clientID); err != nil {
		return err
	}

	// 3. Delete default relations from Keto via Outbox.
	s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionDelete, client, tenantID)

	return nil
}

func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
	if client == nil {
		return nil
	}
	rp := &domain.RelyingParty{
		ClientID: client.ClientID,
		Name:     client.ClientName,
	}
	if client.Metadata != nil {
		if tid, ok := client.Metadata["tenant_id"].(string); ok {
			rp.TenantID = tid
		}
		if desc, ok := client.Metadata["description"].(string); ok {
			rp.Description = desc
		}
	}
	return rp
}
