package service

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/utils"
	"context"
	"errors"
	"log/slog"
	"strings"

	"gorm.io/gorm"
)

type TenantService interface {
	RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error)
	RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error)
	GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
	GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
	GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
	ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
	ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
	ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
	IsDomainAllowed(ctx context.Context, domainName string) (bool, error)
	ApproveTenant(ctx context.Context, id string) error
	ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
	SetKetoService(keto KetoService)
	DeleteTenantsBulk(ctx context.Context, ids []string) error
}

type tenantService struct {
	repo          repository.TenantRepository
	userRepo      repository.UserRepository
	userGroupRepo repository.UserGroupRepository
	keto          KetoService
	outboxRepo    repository.KetoOutboxRepository
}

func NewTenantService(repo repository.TenantRepository, userRepo repository.UserRepository, userGroupRepo repository.UserGroupRepository, outboxRepo repository.KetoOutboxRepository) TenantService {
	return &tenantService{
		repo:          repo,
		userRepo:      userRepo,
		userGroupRepo: userGroupRepo,
		outboxRepo:    outboxRepo,
	}
}

func (s *tenantService) SetKetoService(keto KetoService) {
	s.keto = keto
}

func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
	return s.repo.FindByID(ctx, id)
}

func (s *tenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
	if s.keto == nil {
		return nil, errors.New("keto service not initialized")
	}

	allIDs, err := s.keto.ListObjects(ctx, "Tenant", "manage", "User:"+userID)
	if err != nil {
		slog.Error("Failed to list manageable tenants from Keto", "userID", userID, "error", err)
		return []domain.Tenant{}, nil
	}

	if len(allIDs) == 0 {
		directAdminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
		directOwnerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)

		idMap := make(map[string]bool)
		for _, id := range directAdminIDs {
			idMap[id] = true
		}
		for _, id := range directOwnerIDs {
			idMap[id] = true
		}

		allIDs = make([]string, 0, len(idMap))
		for id := range idMap {
			allIDs = append(allIDs, id)
		}
	}

	if len(allIDs) == 0 {
		return []domain.Tenant{}, nil
	}

	return s.repo.FindByIDs(ctx, allIDs)
}

func (s *tenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
	if s.keto == nil {
		return nil, errors.New("keto service not initialized")
	}

	memberIDs, err := s.keto.ListObjects(ctx, "Tenant", "members", "User:"+userID)
	if err != nil {
		slog.Error("Failed to list joined tenants from Keto", "userID", userID, "error", err)
		return []domain.Tenant{}, nil
	}

	ownerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)
	adminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)

	idMap := make(map[string]bool)
	for _, id := range memberIDs {
		idMap[id] = true
	}
	for _, id := range ownerIDs {
		idMap[id] = true
	}
	for _, id := range adminIDs {
		idMap[id] = true
	}

	allIDs := make([]string, 0, len(idMap))
	for id := range idMap {
		allIDs = append(allIDs, id)
	}

	if len(allIDs) == 0 {
		return []domain.Tenant{}, nil
	}

	return s.repo.FindByIDs(ctx, allIDs)
}

func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) {
	if ok, msg := utils.ValidateSlug(slug); !ok {
		return nil, errors.New(msg)
	}

	existing, err := s.repo.FindBySlug(ctx, slug)
	if err == nil && existing != nil {
		return nil, errors.New("tenant slug already exists")
	}
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	tenant := &domain.Tenant{
		Type:        tenantType,
		Name:        name,
		Slug:        slug,
		Description: description,
		Status:      domain.TenantStatusActive,
		ParentID:    parentID,
	}

	if err := s.repo.Create(ctx, tenant); err != nil {
		return nil, err
	}

	if s.outboxRepo != nil {
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenant.ID,
			Relation:  "admins",
			Subject:   "System:global#super_admins",
			Action:    domain.KetoOutboxActionCreate,
		})

		if tenant.ParentID != nil {
			if err := s.outboxRepo.Create(ctx, &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    tenant.ID,
				Relation:  "parents",
				Subject:   "Tenant:" + *tenant.ParentID,
				Action:    domain.KetoOutboxActionCreate,
			}); err != nil {
				slog.Error("Failed to create outbox entry for tenant hierarchy", "tenant", tenant.ID, "error", err)
			}
		}

		if creatorID != "" {
			slog.Info("Creating outbox entries for tenant creator", "tenant", tenant.ID, "creator", creatorID)
			_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    tenant.ID,
				Relation:  "owners",
				Subject:   "User:" + creatorID,
				Action:    domain.KetoOutboxActionCreate,
			})
			_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    tenant.ID,
				Relation:  "admins",
				Subject:   "User:" + creatorID,
				Action:    domain.KetoOutboxActionCreate,
			})
			_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    tenant.ID,
				Relation:  "members",
				Subject:   "User:" + creatorID,
				Action:    domain.KetoOutboxActionCreate,
			})
		}
	}

	for _, d := range domains {
		if err := s.repo.AddDomain(ctx, tenant.ID, d, true); err != nil {
			slog.Error("Failed to add domain to tenant", "tenant", slug, "domain", d, "error", err)
		}
	}

	return s.repo.FindBySlug(ctx, slug)
}

func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
	if ok, msg := utils.ValidateSlug(slug); !ok {
		return nil, errors.New(msg)
	}

	parts := strings.Split(adminEmail, "@")
	if len(parts) != 2 || parts[1] != domainName {
		return nil, errors.New("admin email domain must match the tenant domain")
	}

	tenant := &domain.Tenant{
		Type:        domain.TenantTypeCompany,
		Name:        name,
		Slug:        slug,
		Description: description,
		Status:      domain.TenantStatusPending,
		Config:      domain.JSONMap{"adminEmail": adminEmail},
	}

	if err := s.repo.Create(ctx, tenant); err != nil {
		return nil, err
	}

	if s.outboxRepo != nil {
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenant.ID,
			Relation:  "admins",
			Subject:   "System:global#super_admins",
			Action:    domain.KetoOutboxActionCreate,
		})
	}

	if err := s.repo.AddDomain(ctx, tenant.ID, domainName, false); err != nil {
		return nil, err
	}

	return tenant, nil
}

func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
	tenant, err := s.repo.FindByID(ctx, id)
	if err != nil {
		return err
	}

	tenant.Status = domain.TenantStatusActive
	if err := s.repo.Update(ctx, tenant); err != nil {
		return err
	}

	if s.outboxRepo != nil {
		if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
			slog.Info("Queueing tenant admin/owner sync to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
			if s.userRepo != nil {
				user, err := s.userRepo.FindByEmail(ctx, adminEmail)
				if err == nil && user != nil {
					slog.Info("Queueing tenant ownership/membership sync to Keto", "tenant", tenant.Slug, "userID", user.ID)
					_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
						Namespace: "Tenant",
						Object:    tenant.ID,
						Relation:  "owners",
						Subject:   "User:" + user.ID,
						Action:    domain.KetoOutboxActionCreate,
					})
					_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
						Namespace: "Tenant",
						Object:    tenant.ID,
						Relation:  "admins",
						Subject:   "User:" + user.ID,
						Action:    domain.KetoOutboxActionCreate,
					})
					_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
						Namespace: "Tenant",
						Object:    tenant.ID,
						Relation:  "members",
						Subject:   "User:" + user.ID,
						Action:    domain.KetoOutboxActionCreate,
					})
				} else {
					slog.Info("Tenant admin user not found in local DB, will need manual sync or sync on signup", "email", adminEmail)
				}
			}
		}
	}

	return nil
}

func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
	tenant, err := s.repo.FindByDomain(ctx, emailDomain)
	if err != nil {
		return nil, err
	}

	if tenant.Status != domain.TenantStatusActive {
		return nil, errors.New("tenant is not active")
	}

	return tenant, nil
}

func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
	return s.repo.FindBySlug(ctx, slug)
}

func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
	return s.repo.List(ctx, limit, offset, parentID)
}

func (s *tenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
	tenant, err := s.repo.FindByDomain(ctx, domainName)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		return false, err
	}
	return tenant != nil && tenant.Status == domain.TenantStatusActive, nil
}

func (s *tenantService) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
	groups, err := s.repo.ListByType(ctx, domain.TenantTypeCompanyGroup)
	if err != nil {
		return nil, err
	}

	for _, g := range groups {
		rawConfig, ok := g.Config["autoProvisioning"].(map[string]interface{})
		if !ok {
			continue
		}

		enabled, _ := rawConfig["enabled"].(bool)
		if !enabled {
			continue
		}

		mapping, ok := rawConfig["mappingRules"].(map[string]interface{})
		if !ok {
			continue
		}

		rule, ok := mapping[domainName].(map[string]interface{})
		if !ok {
			continue
		}

		slug, _ := rule["slug"].(string)
		name, _ := rule["name"].(string)

		if slug == "" || name == "" {
			continue
		}

		slog.Info("[Provisioning] Found rule for domain, creating sub-tenant", "domain", domainName, "parent", g.Slug, "newTenant", slug)
		return s.RegisterTenant(ctx, name, slug, domain.TenantTypeCompany, "Automatically provisioned via group policy", []string{domainName}, &g.ID, "")
	}

	return nil, gorm.ErrRecordNotFound
}

func (s *tenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error {
	if len(ids) == 0 {
		return nil
	}

	if err := s.repo.DeleteBulk(ctx, ids); err != nil {
		return err
	}

	if s.outboxRepo != nil {
		for _, id := range ids {
			_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    id,
				Relation:  "parents",
				Action:    domain.KetoOutboxActionDelete,
			})
		}
	}

	return nil
}
