package service

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

	"github.com/google/uuid"
)

type UserGroupService interface {
	Create(ctx context.Context, tenantID string, parentID *string, name, description, unitType string) (*domain.UserGroup, error)
	Get(ctx context.Context, id string) (*domain.UserGroup, error)
	List(ctx context.Context, tenantID string) ([]domain.UserGroup, error)
	Delete(ctx context.Context, tenantID, groupID string) error
	Update(ctx context.Context, tenantID, groupID string, name, description, unitType string, parentID *string) (*domain.UserGroup, error)
	SetWorksmobileSyncer(syncer WorksmobileSyncer)

	// Member Management with Keto Sync
	AddMember(ctx context.Context, groupID, userID string) error
	RemoveMember(ctx context.Context, groupID, userID string) error

	// Permission Management
	ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error)
	AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error
	RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error
}

type userGroupService struct {
	repo        repository.UserGroupRepository
	userRepo    repository.UserRepository
	tenantRepo  repository.TenantRepository
	ketoService KetoService
	outboxRepo  repository.KetoOutboxRepository
	kratos      KratosAdminService
	worksmobile WorksmobileSyncer
}

func NewUserGroupService(
	repo repository.UserGroupRepository,
	userRepo repository.UserRepository,
	tenantRepo repository.TenantRepository,
	keto KetoService,
	outbox repository.KetoOutboxRepository,
	kratos KratosAdminService,
) UserGroupService {
	return &userGroupService{
		repo:        repo,
		userRepo:    userRepo,
		tenantRepo:  tenantRepo,
		ketoService: keto,
		outboxRepo:  outbox,
		kratos:      kratos,
	}
}

func (s *userGroupService) SetWorksmobileSyncer(syncer WorksmobileSyncer) {
	s.worksmobile = syncer
}

func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID *string, name, description, unitType string) (*domain.UserGroup, error) {
	// For Keto and Tenant hierarchy, if no parent group, the company tenant is the parent.
	actualParentID := parentID
	if actualParentID == nil || *actualParentID == "" {
		actualParentID = &tenantID
	}

	// Validate parent tenant exists
	if _, err := s.tenantRepo.FindByID(ctx, *actualParentID); err != nil {
		return nil, fmt.Errorf("parent tenant not found or invalid: %w", err)
	}

	unitID := uuid.NewString()

	// 1. Create Tenant (Type: ORGANIZATION)
	groupTenant := &domain.Tenant{
		ID:          unitID,
		Type:        domain.TenantTypeOrganization,
		ParentID:    actualParentID,
		Name:        name,
		Slug:        fmt.Sprintf("ug-%s", unitID[:8]),
		Description: description,
		Status:      domain.TenantStatusActive,
	}

	if err := s.tenantRepo.Create(ctx, groupTenant); err != nil {
		slog.Error("Failed to create tenant record for user group", "error", err)
		return nil, err
	}

	// 2. Create UserGroup metadata
	// parent_id in user_groups refers to other groups, so use original parentID (which might be nil)
	group := &domain.UserGroup{
		ID:          unitID,
		TenantID:    tenantID,
		ParentID:    parentID,
		Name:        name,
		Description: description,
		UnitType:    unitType,
	}

	if err := s.repo.Create(ctx, group); err != nil {
		// Rollback Tenant creation? Or handle via cleanup job. For now, just log.
		slog.Error("Failed to create user group metadata after creating tenant", "tenantId", unitID, "error", err)
		return nil, err
	}

	// 3. Keto Hierarchy via Outbox: Tenant:<child_id>#parents@Tenant:<parent_id>
	if s.outboxRepo != nil {
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    unitID,
			Relation:  "parents",
			Subject:   "Tenant:" + *actualParentID,
			Action:    domain.KetoOutboxActionCreate,
		})
	}

	return group, nil
}

func (s *userGroupService) Update(ctx context.Context, tenantID, groupID string, name, description, unitType string, parentID *string) (*domain.UserGroup, error) {
	// Implementation for Update
	return nil, nil // Placeholder
}

func (s *userGroupService) Delete(ctx context.Context, tenantID, groupID string) error {
	// Implementation for Delete
	return nil // Placeholder
}

func (s *userGroupService) populateMembers(ctx context.Context, group *domain.UserGroup) {
	tuples, err := s.ketoService.ListRelations(ctx, "Tenant", group.ID, "members", "")
	if err != nil {
		slog.Error("Failed to fetch group members from keto", "error", err, "group_id", group.ID)
		group.Members = []domain.User{}
		return
	}

	var userIDs []string
	for _, t := range tuples {
		sid := t.SubjectID
		if len(sid) > 5 && sid[:5] == "User:" {
			userIDs = append(userIDs, sid[5:])
		} else {
			userIDs = append(userIDs, sid)
		}
	}

	if len(userIDs) > 0 {
		members, err := s.userRepo.FindByIDs(ctx, userIDs)
		if err != nil {
			slog.Error("Failed to fetch member details from db", "error", err)
		}

		memberMap := make(map[string]domain.User)
		for _, m := range members {
			memberMap[m.ID] = m
		}

		var finalMembers []domain.User
		for _, uid := range userIDs {
			if m, ok := memberMap[uid]; ok {
				finalMembers = append(finalMembers, m)
			} else if s.kratos != nil {
				identity, err := s.kratos.GetIdentity(ctx, uid)
				if err == nil && identity != nil {
					name, _ := identity.Traits["name"].(string)
					email, _ := identity.Traits["email"].(string)
					finalMembers = append(finalMembers, domain.User{
						ID:    uid,
						Name:  name,
						Email: email,
					})
				}
			}
		}
		group.Members = finalMembers
	} else {
		group.Members = []domain.User{}
	}
}

func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
	group, err := s.repo.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}
	s.populateMembers(ctx, group)
	return group, nil
}

func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.UserGroup, error) {
	groups, err := s.repo.ListByTenantID(ctx, tenantID)
	if err != nil {
		return nil, err
	}

	if s.ketoService == nil {
		return groups, nil
	}

	for i := range groups {
		s.populateMembers(ctx, &groups[i])
	}

	return groups, nil
}

func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string) error {
	// Validate group exists
	group, err := s.repo.FindByID(ctx, groupID)
	if err != nil {
		return fmt.Errorf("user group not found: %w", err)
	}

	var tenant *domain.Tenant
	if s.tenantRepo != nil {
		tenant, _ = s.tenantRepo.FindByID(ctx, group.TenantID)
	}

	// Kratos는 identity SSOT이고 조직/부서 정보의 원장이 아니므로 AddMember에서 traits를 수정하지 않습니다.
	if s.userRepo != nil && tenant != nil {
		localUser, err := s.userRepo.FindByID(ctx, userID)
		if err != nil || localUser == nil {
			if s.kratos != nil {
				identity, identityErr := s.kratos.GetIdentity(ctx, userID)
				if identityErr == nil && identity != nil {
					localUser = mapUserGroupKratosIdentityToLocalUser(*identity)
				} else {
					slog.Warn("Skipping local user sync during AddMember because identity read is unavailable", "user", userID, "error", identityErr)
				}
			} else {
				slog.Warn("Skipping local user sync during AddMember because identity projection is unavailable", "user", userID, "error", err)
			}
		}
		if localUser != nil {
			localUser.TenantID = &tenant.ID
			localUser.Department = group.Name
			if err := s.userRepo.Update(ctx, localUser); err != nil {
				slog.Error("Failed to sync local user during AddMember", "user", userID, "error", err)
			} else if s.worksmobile != nil {
				if err := s.worksmobile.EnqueueUserUpsertIfInScope(ctx, *localUser); err != nil {
					slog.Warn("Failed to enqueue Worksmobile user sync during AddMember", "user", userID, "error", err)
				}
			}
		}
	}

	// Keto via Outbox: Tenant:<groupID>#members@User:<userID>
	if s.outboxRepo != nil {
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    groupID,
			Relation:  "members",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})

		// Also add direct Tenant membership to Keto for member counting
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    group.TenantID,
			Relation:  "members",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})
	}

	return nil
}

func mapUserGroupKratosIdentityToLocalUser(identity KratosIdentity) *domain.User {
	traits := identity.Traits
	now := time.Now()
	createdAt := identity.CreatedAt
	if createdAt.IsZero() {
		createdAt = now
	}
	updatedAt := identity.UpdatedAt
	if updatedAt.IsZero() {
		updatedAt = now
	}

	role, ok := domain.NormalizeRoleAlias(userGroupTraitString(traits, "role"))
	if !ok {
		role, ok = domain.NormalizeRoleAlias(userGroupTraitString(traits, "grade"))
		if !ok {
			role = domain.RoleUser
		}
	}
	grade := userGroupTraitString(traits, "grade")
	if _, ok := domain.NormalizeRoleAlias(grade); ok {
		grade = ""
	}

	user := &domain.User{
		ID:              identity.ID,
		Email:           userGroupTraitString(traits, "email"),
		Name:            userGroupTraitString(traits, "name"),
		Phone:           domain.NormalizePhoneNumber(userGroupTraitString(traits, "phone_number")),
		Role:            role,
		Status:          userGroupIdentityStatus(identity.State),
		Department:      userGroupTraitString(traits, "department"),
		Grade:           grade,
		Position:        userGroupTraitString(traits, "position"),
		JobTitle:        userGroupTraitString(traits, "jobTitle"),
		AffiliationType: userGroupTraitString(traits, "affiliationType"),
		CreatedAt:       createdAt,
		UpdatedAt:       updatedAt,
		Metadata:        make(domain.JSONMap),
	}
	if tenantID := userGroupTraitString(traits, "tenant_id"); tenantID != "" {
		user.TenantID = &tenantID
	}
	if relyingPartyID := userGroupTraitString(traits, "relying_party_id"); relyingPartyID != "" {
		user.RelyingPartyID = &relyingPartyID
	}
	coreTraits := map[string]bool{
		"email": true, "name": true, "phone_number": true,
		"grade": true, "role": true, "companyCode": true, "company_code": true,
		"companyCodes": true, "tenant_id": true, "department": true,
		"position": true, "jobTitle": true, "affiliationType": true,
		"relying_party_id": true, "custom_login_ids": true, "id": true,
	}
	for key, value := range traits {
		if !coreTraits[key] {
			user.Metadata[key] = value
		}
	}
	return user
}

func userGroupTraitString(traits map[string]any, key string) string {
	if traits == nil {
		return ""
	}
	value, ok := traits[key]
	if !ok || value == nil {
		return ""
	}
	if str, ok := value.(string); ok {
		return str
	}
	return fmt.Sprint(value)
}

func userGroupTraitStringArray(traits map[string]any, key string) []string {
	if traits == nil {
		return nil
	}
	switch value := traits[key].(type) {
	case []string:
		return value
	case []any:
		items := make([]string, 0, len(value))
		for _, item := range value {
			if str, ok := item.(string); ok && str != "" {
				items = append(items, str)
			}
		}
		return items
	default:
		return nil
	}
}

func userGroupIdentityStatus(state string) string {
	return domain.NormalizeUserStatus(state)
}

func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
	// Validate group exists
	if _, err := s.repo.FindByID(ctx, groupID); err != nil {
		return fmt.Errorf("user group not found: %w", err)
	}

	// Keto via Outbox: Delete relation
	if s.outboxRepo != nil {
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    groupID,
			Relation:  "members",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionDelete,
		})
	}

	return nil
}

func (s *userGroupService) ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error) {
	// Query: namespace=Tenant, subject=Tenant:groupID#members
	subject := "Tenant:" + groupID + "#members"
	tuples, err := s.ketoService.ListRelations(ctx, "Tenant", "", "", subject)
	if err != nil {
		slog.Error("Failed to fetch group roles from keto", "error", err, "group_id", groupID)
		return nil, err
	}

	var roles []domain.GroupRole
	tenantIDs := make([]string, 0, len(tuples))
	for _, t := range tuples {
		tenantIDs = append(tenantIDs, t.Object)
	}

	if len(tenantIDs) > 0 {
		tenantList, err := s.tenantRepo.FindByIDs(ctx, tenantIDs)
		if err != nil {
			slog.Error("Failed to fetch tenant details for roles", "error", err)
		}

		tenantMap := make(map[string]string)
		for _, t := range tenantList {
			tenantMap[t.ID] = t.Name
		}

		for _, t := range tuples {
			roles = append(roles, domain.GroupRole{
				TenantID:   t.Object,
				TenantName: tenantMap[t.Object],
				Relation:   t.Relation,
			})
		}
	}

	return roles, nil
}

func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error {
	// Validate group exists
	if _, err := s.repo.FindByID(ctx, groupID); err != nil {
		return fmt.Errorf("user group not found: %w", err)
	}

	// Keto via Outbox: Tenant:<tenantID>#<relation>@Tenant:<groupID>#members
	if s.outboxRepo != nil {
		subject := "Tenant:" + groupID + "#members"
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  relation,
			Subject:   subject,
			Action:    domain.KetoOutboxActionCreate,
		})
	}
	return nil
}

func (s *userGroupService) RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error {
	// Keto via Outbox: Delete relation
	if s.outboxRepo != nil {
		subject := "Tenant:" + groupID + "#members"
		_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  relation,
			Subject:   subject,
			Action:    domain.KetoOutboxActionDelete,
		})
	}
	return nil
}
