package handler

import (
	"baron-sso-backend/internal/bootstrap"
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/pagination"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"baron-sso-backend/internal/utils"
	"bytes"
	"context"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"

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

type TenantHandler struct {
	DB                 *gorm.DB
	Service            service.TenantService
	UserRepo           repository.UserRepository
	UserProjectionRepo repository.UserProjectionRepository
	Keto               service.KetoService
	KetoOutbox         repository.KetoOutboxRepository
	KratosAdmin        service.KratosAdminService
	SharedLink         service.SharedLinkService
	Worksmobile        service.WorksmobileSyncer
}

func seedTenantDeleteError(c *fiber.Ctx) error {
	return errorJSON(c, fiber.StatusConflict, "seed tenants cannot be deleted")
}

func seedTenantSlugsForDeleteGuard() []string {
	slugs, err := bootstrap.SeedTenantSlugSet()
	if err != nil {
		return nil
	}

	result := make([]string, 0, len(slugs))
	for slug := range slugs {
		result = append(result, slug)
	}
	return result
}

func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repository.UserRepository, userProjectionRepo repository.UserProjectionRepository, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService, sharedLink service.SharedLinkService) *TenantHandler {
	return &TenantHandler{
		DB:                 db,
		Service:            svc,
		UserRepo:           userRepo,
		UserProjectionRepo: userProjectionRepo,
		Keto:               keto,
		KetoOutbox:         outbox,
		KratosAdmin:        kratos,
		SharedLink:         sharedLink,
	}
}

func (h *TenantHandler) SetWorksmobileSyncer(syncer service.WorksmobileSyncer) {
	h.Worksmobile = syncer
}

type tenantSummary struct {
	ID          string         `json:"id"`
	Type        string         `json:"type"`
	ParentID    *string        `json:"parentId"`
	Name        string         `json:"name"`
	Slug        string         `json:"slug"`
	Description string         `json:"description"`
	Status      string         `json:"status"`
	Domains     []string       `json:"domains,omitempty"`
	Config      domain.JSONMap `json:"config,omitempty"`
	MemberCount int64          `json:"memberCount"`
	CreatedAt   string         `json:"createdAt"`
	UpdatedAt   string         `json:"updatedAt"`
}

type tenantListResponse struct {
	Items      []tenantSummary `json:"items"`
	Limit      int             `json:"limit"`
	Offset     int             `json:"offset"`
	Total      int64           `json:"total"`
	Cursor     string          `json:"cursor,omitempty"`
	NextCursor string          `json:"nextCursor,omitempty"`
}

func pageTenantsByCursor(tenants []domain.Tenant, limit int, cursorRaw string) ([]domain.Tenant, string, error) {
	ordered := append([]domain.Tenant(nil), tenants...)
	pagination.SortByKeyDesc(ordered, func(tenant domain.Tenant) (time.Time, string) {
		return tenant.CreatedAt, tenant.ID
	})
	return pagination.PageByCursor(ordered, limit, cursorRaw, func(tenant domain.Tenant) (time.Time, string) {
		return tenant.CreatedAt, tenant.ID
	})
}

type tenantImportResult struct {
	Created int      `json:"created"`
	Updated int      `json:"updated"`
	Failed  int      `json:"failed"`
	Errors  []string `json:"errors"`
}

type tenantDomainConflict struct {
	Domain     string `json:"domain"`
	TenantID   string `json:"tenantId"`
	TenantName string `json:"tenantName"`
	TenantSlug string `json:"tenantSlug"`
}

type tenantCSVRecord struct {
	TenantID         string
	Name             string
	Type             string
	ParentTenantID   *string
	ParentTenantSlug string
	Slug             string
	Memo             string
	Domains          []string
	Visibility       string
	OrgUnitType      string
}

type orgContextTenant struct {
	ID          string             `json:"id"`
	Type        string             `json:"type"`
	Name        string             `json:"name"`
	Slug        string             `json:"slug"`
	ParentID    *string            `json:"parentId"`
	Status      string             `json:"status"`
	Description string             `json:"description"`
	Domains     []string           `json:"domains,omitempty"`
	MemberCount int64              `json:"memberCount"`
	Visibility  string             `json:"visibility"`
	OrgUnitType string             `json:"orgUnitType,omitempty"`
	Config      domain.JSONMap     `json:"config,omitempty"`
	CreatedAt   string             `json:"createdAt"`
	UpdatedAt   string             `json:"updatedAt"`
	Members     []orgContextMember `json:"members"`
}

type orgContextMember struct {
	ID         string `json:"id,omitempty"`
	Email      string `json:"email"`
	Name       string `json:"name"`
	Phone      string `json:"phone,omitempty"`
	Department string `json:"department,omitempty"`
	Grade      string `json:"grade,omitempty"`
	Position   string `json:"position,omitempty"`
	JobTitle   string `json:"jobTitle,omitempty"`
	IsOwner    bool   `json:"isOwner"`
	IsLeader   bool   `json:"isLeader"`
	IsPrimary  bool   `json:"isPrimary"`
}

type orgContextMemberAssignment struct {
	TenantID string
	Member   orgContextMember
}

type orgContextTreeNode struct {
	orgContextTenant
	Children []orgContextTreeNode `json:"children"`
}

type orgContextScope struct {
	TenantID   string `json:"tenantId"`
	TenantSlug string `json:"tenantSlug"`
}

type orgContextResponse struct {
	SchemaVersion string              `json:"schemaVersion"`
	IssuedAt      string              `json:"issuedAt"`
	Scope         orgContextScope     `json:"scope"`
	Tree          *orgContextTreeNode `json:"tree"`
	Tenants       []orgContextTenant  `json:"tenants"`
}

func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
	var req struct {
		Name        string `json:"name"`
		Slug        string `json:"slug"`
		Description string `json:"description"`
		Domain      string `json:"domain"`
		AdminEmail  string `json:"adminEmail"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	// Basic validation
	if req.Name == "" || req.Domain == "" || req.AdminEmail == "" {
		return errorJSON(c, fiber.StatusBadRequest, "name, domain, and adminEmail are required")
	}

	tenant, err := h.Service.RequestRegistration(c.Context(), req.Name, req.Slug, req.Description, req.Domain, req.AdminEmail)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}

	return c.Status(fiber.StatusAccepted).JSON(fiber.Map{
		"message": "Registration request received and is pending approval.",
		"tenant":  mapTenantSummary(*tenant),
	})
}

func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	if err := h.Service.ApproveTenant(c.Context(), tenantID); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.JSON(fiber.Map{"message": "Tenant approved successfully"})
}

func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
	limit := c.QueryInt("limit", 50)
	offset := c.QueryInt("offset", 0)
	parentId := c.Query("parentId")
	cursorRaw := strings.TrimSpace(c.Query("cursor"))

	if limit <= 0 {
		limit = 50
	}
	if offset < 0 {
		offset = 0
	}

	var tenants []domain.Tenant
	var total int64
	var err error
	nextCursor := ""

	profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
	role := ""
	if profile != nil {
		role = domain.NormalizeRole(profile.Role)
	}

	if role != domain.RoleSuperAdmin {
		// Not a super admin: Only return the entire tree(s) of the tenants they belong to
		allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
		}

		if profile != nil {
			baseTenantIDs := []string{}
			for _, t := range profile.ManageableTenants {
				baseTenantIDs = append(baseTenantIDs, t.ID)
			}
			for _, t := range profile.JoinedTenants {
				baseTenantIDs = append(baseTenantIDs, t.ID)
			}
			if profile.TenantID != nil {
				baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
			}

			parentMap := make(map[string]string)
			for _, t := range allTenants {
				if t.ParentID != nil {
					parentMap[t.ID] = *t.ParentID
				}
			}

			findRoot := func(id string) string {
				curr := id
				for {
					p, exists := parentMap[curr]
					if !exists || p == "" {
						break
					}
					curr = p
				}
				return curr
			}

			roots := make(map[string]bool)
			for _, id := range baseTenantIDs {
				roots[findRoot(id)] = true
			}

			// Filter tenants that belong to the same tree family
			for _, t := range allTenants {
				if roots[findRoot(t.ID)] {
					tenants = append(tenants, t)
				}
			}
		}

		tenants, err = h.filterPrivateTenantsForProfile(c.Context(), tenants, profile)
		if err != nil {
			return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
		}

		total = int64(len(tenants))
		if cursorRaw != "" {
			tenants, nextCursor, err = pageTenantsByCursor(tenants, limit, cursorRaw)
			if err != nil {
				return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
			}
			offset = 0
		} else if offset < len(tenants) {
			end := offset + limit
			if end > len(tenants) {
				end = len(tenants)
			}
			tenants = tenants[offset:end]
			if total > int64(end) && len(tenants) > 0 {
				last := tenants[len(tenants)-1]
				nextCursor = pagination.Encode(last.CreatedAt, last.ID)
			}
		} else {
			tenants = []domain.Tenant{}
		}
	} else {
		// Super Admin case
		if cursorRaw != "" && h.DB != nil {
			tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw)
			if err != nil {
				return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
			}
			offset = 0
		} else {
			tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
			if err != nil {
				return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
			}
			if total > int64(offset+len(tenants)) && len(tenants) > 0 {
				last := tenants[len(tenants)-1]
				nextCursor = pagination.Encode(last.CreatedAt, last.ID)
			}
		}
	}

	memberCounts, err := h.countTenantMembersFromProjection(c.Context(), tenants)
	if err != nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
	}

	items := make([]tenantSummary, 0, len(tenants))
	for _, t := range tenants {
		summary := mapTenantSummary(t)
		summary.MemberCount = memberCounts[t.ID]
		items = append(items, summary)
	}

	return c.JSON(tenantListResponse{
		Items:      items,
		Limit:      limit,
		Offset:     offset,
		Total:      total,
		Cursor:     cursorRaw,
		NextCursor: nextCursor,
	})
}

func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string) ([]domain.Tenant, int64, string, error) {
	cursor, err := pagination.Decode(cursorRaw)
	if err != nil {
		return nil, 0, "", err
	}

	countQuery := h.DB.WithContext(ctx).Model(&domain.Tenant{})
	pageQuery := h.DB.WithContext(ctx).Model(&domain.Tenant{})
	if parentID != "" {
		countQuery = countQuery.Where("parent_id = ?", parentID)
		pageQuery = pageQuery.Where("parent_id = ?", parentID)
	}

	var total int64
	if err := countQuery.Count(&total).Error; err != nil {
		return nil, 0, "", err
	}

	pageQuery = pagination.ApplyCreatedAtIDCursor(pageQuery, cursor, "created_at", "id")

	var tenants []domain.Tenant
	if err := pageQuery.
		Order("created_at desc, id desc").
		Limit(limit + 1).
		Preload("Domains").
		Find(&tenants).Error; err != nil {
		return nil, 0, "", err
	}

	nextCursor := ""
	if len(tenants) > limit {
		tenants = tenants[:limit]
		last := tenants[len(tenants)-1]
		nextCursor = pagination.Encode(last.CreatedAt, last.ID)
	}
	return tenants, total, nextCursor, nil
}

func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
	parentID := strings.TrimSpace(c.Query("parentId"))
	allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
	allTenants, err = h.filterPrivateTenantsForProfile(c.Context(), allTenants, profile)
	if err != nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
	}
	tenants := filterTenantCSVDescendants(allTenants, parentID)
	sortTenantsByInputOrder(tenants)

	var buf bytes.Buffer
	writer := csv.NewWriter(&buf)
	includeIDs := includeCSVIds(c)
	if includeIDs {
		if err := writer.Write([]string{"tenant_id", "name", "type", "parent_tenant_id", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
	} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	slugByID := make(map[string]string, len(allTenants))
	for _, tenant := range allTenants {
		slugByID[tenant.ID] = tenant.Slug
	}
	for _, tenant := range tenants {
		parentID := ""
		parentSlug := ""
		if tenant.ParentID != nil {
			parentID = *tenant.ParentID
			parentSlug = slugByID[parentID]
		}
		domains := make([]string, 0, len(tenant.Domains))
		for _, domainName := range tenant.Domains {
			domainName := strings.TrimSpace(domainName.Domain)
			if domainName != "" {
				domains = append(domains, domainName)
			}
		}
		visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
		row := []string{
			tenant.Name,
			tenant.Type,
			parentSlug,
			tenant.Slug,
			tenant.Description,
			strings.Join(domains, ";"),
			visibility,
			orgUnitType,
		}
		if includeIDs {
			row = []string{
				tenant.ID,
				tenant.Name,
				tenant.Type,
				parentID,
				parentSlug,
				tenant.Slug,
				tenant.Description,
				strings.Join(domains, ";"),
				visibility,
				orgUnitType,
			}
		}
		if err := writer.Write(row); err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
	}
	writer.Flush()
	if err := writer.Error(); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	c.Set(fiber.HeaderContentType, "text/csv")
	c.Set(fiber.HeaderContentDisposition, `attachment; filename="tenants.csv"`)
	return c.Send(buf.Bytes())
}

func sortTenantsByInputOrder(tenants []domain.Tenant) {
	sort.SliceStable(tenants, func(i, j int) bool {
		if tenants[i].CreatedAt.Equal(tenants[j].CreatedAt) {
			return tenants[i].ID < tenants[j].ID
		}
		return tenants[i].CreatedAt.Before(tenants[j].CreatedAt)
	})
}

func filterTenantCSVDescendants(tenants []domain.Tenant, parentID string) []domain.Tenant {
	parentID = strings.TrimSpace(parentID)
	if parentID == "" {
		return tenants
	}

	descendantIDs := map[string]bool{}
	frontier := map[string]bool{parentID: true}
	for len(frontier) > 0 {
		next := map[string]bool{}
		for _, tenant := range tenants {
			if tenant.ParentID == nil {
				continue
			}
			if !frontier[strings.TrimSpace(*tenant.ParentID)] {
				continue
			}
			if descendantIDs[tenant.ID] {
				continue
			}
			descendantIDs[tenant.ID] = true
			next[tenant.ID] = true
		}
		frontier = next
	}

	filtered := make([]domain.Tenant, 0, len(descendantIDs))
	for _, tenant := range tenants {
		if descendantIDs[tenant.ID] {
			filtered = append(filtered, tenant)
		}
	}
	return filtered
}

func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
	reader, err := tenantCSVReaderFromRequest(c)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}

	records, err := parseTenantCSVRecords(reader)
	if err != nil {
		return errorJSON(c, fiber.StatusBadRequest, err.Error())
	}
	records = orderTenantCSVRecordsByParentSlug(records)

	creatorID := ""
	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
		creatorID = profile.ID
	}

	tenantIDBySlug := make(map[string]string)
	if h.Service != nil {
		if tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, ""); err == nil {
			for _, tenant := range tenants {
				tenantIDBySlug[strings.ToLower(tenant.Slug)] = tenant.ID
			}
		}
	}

	result := tenantImportResult{Errors: make([]string, 0)}
	for i, record := range records {
		rowNumber := i + 2
		if record.ParentTenantID == nil && record.ParentTenantSlug != "" {
			parentID := tenantIDBySlug[strings.ToLower(record.ParentTenantSlug)]
			if parentID == "" {
				result.Failed++
				result.Errors = append(result.Errors, fmt.Sprintf("row %d: parent tenant slug not found: %s", rowNumber, record.ParentTenantSlug))
				continue
			}
			record.ParentTenantID = &parentID
		}
		if record.TenantID != "" || (h.DB != nil && record.Slug != "") {
			tenant, updated, err := h.upsertTenantCSVRecord(c, record)
			if err != nil {
				result.Failed++
				result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error()))
				continue
			}
			if updated {
				tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID
				result.Updated++
				if h.Worksmobile != nil {
					_ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant)
				}
				continue
			}
		}

		recordCreatorID := creatorID
		if record.Type == domain.TenantTypeOrganization {
			recordCreatorID = ""
		}

		tenant, err := h.createTenantCSVRecord(c, record, recordCreatorID)
		if err != nil {
			result.Failed++
			result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error()))
			continue
		}
		if tenant == nil {
			result.Failed++
			result.Errors = append(result.Errors, fmt.Sprintf("row %d: tenant creation returned empty result", rowNumber))
			continue
		}
		tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID
		result.Created++
		if h.Worksmobile != nil {
			_ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant)
		}
	}

	return c.JSON(result)
}

func tenantCSVReaderFromRequest(c *fiber.Ctx) (io.Reader, error) {
	file, err := c.FormFile("file")
	if err == nil && file != nil {
		opened, err := file.Open()
		if err != nil {
			return nil, errors.New("failed to open uploaded file")
		}
		defer opened.Close()
		data, err := io.ReadAll(opened)
		if err != nil {
			return nil, errors.New("failed to read uploaded file")
		}
		return bytes.NewReader(data), nil
	}

	body := c.Body()
	if len(bytes.TrimSpace(body)) == 0 {
		return nil, errors.New("csv file is required")
	}
	return bytes.NewReader(body), nil
}

func parseTenantCSVRecords(r io.Reader) ([]tenantCSVRecord, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return nil, errors.New("failed to read csv")
	}
	data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})

	reader := csv.NewReader(bytes.NewReader(data))
	reader.FieldsPerRecord = -1

	rows, err := reader.ReadAll()
	if err != nil {
		return nil, fmt.Errorf("invalid csv: %w", err)
	}
	if len(rows) == 0 {
		return nil, errors.New("csv is empty")
	}

	header := tenantCSVHeaderIndex(rows[0])
	required := []string{"name", "type", "slug"}
	for _, key := range required {
		if _, ok := header[key]; !ok {
			return nil, fmt.Errorf("missing required column: %s", key)
		}
	}

	records := make([]tenantCSVRecord, 0, len(rows)-1)
	for i, row := range rows[1:] {
		if tenantCSVRowIsEmpty(row) {
			continue
		}

		name := tenantCSVValue(row, header, "name")
		if name == "" {
			return nil, fmt.Errorf("row %d: name is required", i+2)
		}

		tenantType := normalizeTenantType(tenantCSVValue(row, header, "type"))
		if tenantType == "" {
			return nil, fmt.Errorf("row %d: invalid tenant type", i+2)
		}

		slug := utils.GenerateSlug(tenantCSVValue(row, header, "slug"))
		if slug == "" {
			return nil, fmt.Errorf("row %d: slug is required", i+2)
		}

		parentValue := tenantCSVValue(row, header, "parent_tenant_id")
		var parentID *string
		if parentValue != "" {
			parentID = &parentValue
		}

		records = append(records, tenantCSVRecord{
			TenantID:         tenantCSVValue(row, header, "tenant_id"),
			Name:             name,
			Type:             tenantType,
			ParentTenantID:   parentID,
			ParentTenantSlug: tenantCSVValue(row, header, "parent_tenant_slug"),
			Slug:             slug,
			Memo:             tenantCSVValue(row, header, "memo"),
			Domains:          splitTenantCSVDomains(tenantCSVValue(row, header, "email_domain")),
			Visibility:       tenantCSVValue(row, header, "visibility"),
			OrgUnitType:      tenantCSVValue(row, header, "org_unit_type"),
		})
	}

	return records, nil
}

func tenantCSVHeaderIndex(header []string) map[string]int {
	index := make(map[string]int, len(header))
	aliases := map[string]string{
		"id":                 "tenant_id",
		"tenantid":           "tenant_id",
		"tenant_id":          "tenant_id",
		"name":               "name",
		"type":               "type",
		"parentid":           "parent_tenant_id",
		"parent_id":          "parent_tenant_id",
		"parenttenantid":     "parent_tenant_id",
		"parent_tenant_id":   "parent_tenant_id",
		"parenttenantslug":   "parent_tenant_slug",
		"parent_tenant_slug": "parent_tenant_slug",
		"slug":               "slug",
		"memo":               "memo",
		"description":        "memo",
		"email-domain":       "email_domain",
		"emaildomain":        "email_domain",
		"email_domain":       "email_domain",
		"domain":             "email_domain",
		"domains":            "email_domain",
		"visibility":         "visibility",
		"public_setting":     "visibility",
		"publicsetting":      "visibility",
		"orgunittype":        "org_unit_type",
		"org_unit_type":      "org_unit_type",
		"org-unit-type":      "org_unit_type",
		"organizationtype":   "org_unit_type",
		"organization_type":  "org_unit_type",
		"orgtype":            "org_unit_type",
		"org_type":           "org_unit_type",
	}
	for i, column := range header {
		key := strings.ToLower(strings.TrimSpace(column))
		key = strings.ReplaceAll(key, " ", "_")
		if canonical, ok := aliases[key]; ok {
			index[canonical] = i
		}
	}
	return index
}

func tenantCSVValue(row []string, header map[string]int, key string) string {
	idx, ok := header[key]
	if !ok || idx >= len(row) {
		return ""
	}
	return strings.TrimSpace(row[idx])
}

func tenantCSVRowIsEmpty(row []string) bool {
	for _, value := range row {
		if strings.TrimSpace(value) != "" {
			return false
		}
	}
	return true
}

func includeCSVIds(c *fiber.Ctx) bool {
	value := strings.ToLower(strings.TrimSpace(c.Query("includeIds")))
	return value == "true" || value == "1" || value == "yes"
}

func orderTenantCSVRecordsByParentSlug(records []tenantCSVRecord) []tenantCSVRecord {
	bySlug := make(map[string]tenantCSVRecord, len(records))
	for _, record := range records {
		bySlug[strings.ToLower(record.Slug)] = record
	}

	ordered := make([]tenantCSVRecord, 0, len(records))
	visited := make(map[string]bool, len(records))
	var visit func(record tenantCSVRecord)
	visit = func(record tenantCSVRecord) {
		key := strings.ToLower(record.Slug)
		if visited[key] {
			return
		}
		if record.ParentTenantSlug != "" {
			if parent, ok := bySlug[strings.ToLower(record.ParentTenantSlug)]; ok {
				visit(parent)
			}
		}
		visited[key] = true
		ordered = append(ordered, record)
	}

	for _, record := range records {
		visit(record)
	}
	return ordered
}

func splitTenantCSVDomains(value string) []string {
	value = strings.ReplaceAll(value, "\n", ";")
	value = strings.ReplaceAll(value, ",", ";")
	parts := strings.Split(value, ";")
	domains := make([]string, 0, len(parts))
	seen := make(map[string]bool, len(parts))
	for _, part := range parts {
		domainName := strings.ToLower(strings.TrimSpace(part))
		if domainName == "" || seen[domainName] {
			continue
		}
		seen[domainName] = true
		domains = append(domains, domainName)
	}
	return domains
}

func normalizeTenantDomainInputs(values []string) []string {
	seen := make(map[string]bool, len(values))
	domains := make([]string, 0, len(values))
	for _, value := range values {
		for _, part := range strings.FieldsFunc(value, func(r rune) bool {
			return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' || r == ' '
		}) {
			domainName := strings.ToLower(strings.TrimSpace(part))
			if domainName == "" || seen[domainName] {
				continue
			}
			seen[domainName] = true
			domains = append(domains, domainName)
		}
	}
	return domains
}

func normalizeTenantConfig(config map[string]any) (domain.JSONMap, error) {
	normalized := make(domain.JSONMap, len(config))
	orgUnitTypeError := "orgUnitType must be one of 실, 팀, TF, TF팀, 센터, 디비전, 셀, 본부, 지역본부, 부, 임원직속"
	for key, value := range config {
		if key == "userSchema" {
			fields, err := normalizeTenantUserSchema(value)
			if err != nil {
				return nil, err
			}
			normalized[key] = fields
			continue
		}
		if key == "visibility" {
			visibility, ok := value.(string)
			if !ok {
				return nil, fmt.Errorf("visibility must be public, internal, or private")
			}
			visibility = strings.TrimSpace(strings.ToLower(visibility))
			if visibility == "" || visibility == "public" {
				normalized[key] = "public"
				continue
			}
			if visibility != "internal" && visibility != "private" {
				return nil, fmt.Errorf("visibility must be public, internal, or private")
			}
			normalized[key] = visibility
			continue
		}
		if key == "orgUnitType" {
			orgUnitType, ok := value.(string)
			if !ok {
				return nil, errors.New(orgUnitTypeError)
			}
			orgUnitType = strings.TrimSpace(orgUnitType)
			if orgUnitType == "" {
				continue
			}
			if !isAllowedOrgUnitType(orgUnitType) {
				return nil, errors.New(orgUnitTypeError)
			}
			normalized[key] = orgUnitType
			continue
		}
		normalized[key] = value
	}
	return normalized, nil
}

func isAllowedOrgUnitType(value string) bool {
	switch value {
	case "실", "팀", "TF", "TF팀", "센터", "디비전", "셀", "본부", "지역본부", "부", "임원직속":
		return true
	default:
		return false
	}
}

func hasTenantOrgConfig(config domain.JSONMap) bool {
	if config == nil {
		return false
	}
	_, hasVisibility := config["visibility"]
	_, hasOrgUnitType := config["orgUnitType"]
	return hasVisibility || hasOrgUnitType
}

func isHanmacFamilyDescendantTenant(tenant domain.Tenant, tenants []domain.Tenant) bool {
	if strings.EqualFold(tenant.Slug, "hanmac-family") {
		return false
	}

	byID := make(map[string]domain.Tenant, len(tenants)+1)
	for _, item := range tenants {
		byID[item.ID] = item
	}
	byID[tenant.ID] = tenant

	parentID := tenant.ParentID
	visited := make(map[string]bool)
	for parentID != nil && *parentID != "" {
		if visited[*parentID] {
			return false
		}
		visited[*parentID] = true

		parent, ok := byID[*parentID]
		if !ok {
			return false
		}
		if strings.EqualFold(parent.Slug, "hanmac-family") {
			return true
		}
		parentID = parent.ParentID
	}
	return false
}

func validateTenantOrgConfigScope(tenant domain.Tenant, tenants []domain.Tenant, config domain.JSONMap) error {
	if !hasTenantOrgConfig(config) {
		return nil
	}
	if isHanmacFamilyDescendantTenant(tenant, tenants) {
		return nil
	}
	return fmt.Errorf("tenant org config is allowed only hanmac-family descendants")
}

func tenantVisibility(config domain.JSONMap) string {
	visibility, _ := config["visibility"].(string)
	switch strings.ToLower(strings.TrimSpace(visibility)) {
	case "internal":
		return "internal"
	case "private":
		return "private"
	default:
		return "public"
	}
}

func tenantCSVOrgConfigValues(config domain.JSONMap) (string, string) {
	visibility := tenantVisibility(config)
	orgUnitType, _ := config["orgUnitType"].(string)
	return visibility, strings.TrimSpace(orgUnitType)
}

func tenantCSVRecordConfig(record tenantCSVRecord) (domain.JSONMap, error) {
	config := map[string]any{}
	if strings.TrimSpace(record.Visibility) != "" {
		config["visibility"] = record.Visibility
	}
	if strings.TrimSpace(record.OrgUnitType) != "" {
		config["orgUnitType"] = record.OrgUnitType
	}
	if len(config) == 0 {
		return nil, nil
	}
	return normalizeTenantConfig(config)
}

func mergeTenantCSVRecordConfig(current domain.JSONMap, record tenantCSVRecord) (domain.JSONMap, bool, error) {
	recordConfig, err := tenantCSVRecordConfig(record)
	if err != nil {
		return nil, false, err
	}
	if len(recordConfig) == 0 {
		return current, false, nil
	}

	merged := make(domain.JSONMap, len(current)+len(recordConfig))
	for key, value := range current {
		merged[key] = value
	}
	for key, value := range recordConfig {
		merged[key] = value
	}
	return merged, true, nil
}

func filterPublicTenants(tenants []domain.Tenant) []domain.Tenant {
	excludedIDs := make(map[string]bool)
	for _, tenant := range tenants {
		visibility := tenantVisibility(tenant.Config)
		if visibility == "internal" || visibility == "private" {
			excludedIDs[tenant.ID] = true
		}
	}

	changed := true
	for changed {
		changed = false
		for _, tenant := range tenants {
			if tenant.ParentID != nil && excludedIDs[*tenant.ParentID] && !excludedIDs[tenant.ID] {
				excludedIDs[tenant.ID] = true
				changed = true
			}
		}
	}

	filtered := make([]domain.Tenant, 0, len(tenants))
	for _, tenant := range tenants {
		if !excludedIDs[tenant.ID] {
			filtered = append(filtered, tenant)
		}
	}
	return filtered
}

func (h *TenantHandler) filterPrivateTenantsForProfile(ctx context.Context, tenants []domain.Tenant, profile *domain.UserProfileResponse) ([]domain.Tenant, error) {
	if profile != nil && domain.NormalizeRole(profile.Role) == domain.RoleSuperAdmin {
		return tenants, nil
	}

	privateRoots := privateTenantRootIDs(tenants)
	if len(privateRoots) == 0 {
		return tenants, nil
	}

	allowedPrivateRoots := make(map[string]bool, len(privateRoots))
	for _, rootID := range privateRoots {
		allowed, err := h.canViewPrivateTenant(ctx, profile, rootID, tenants)
		if err != nil {
			return nil, err
		}
		if allowed {
			allowedPrivateRoots[rootID] = true
		}
	}

	excludedIDs := make(map[string]bool)
	for _, rootID := range privateRoots {
		if !allowedPrivateRoots[rootID] {
			excludedIDs[rootID] = true
		}
	}

	changed := true
	for changed {
		changed = false
		for _, tenant := range tenants {
			if tenant.ParentID != nil && excludedIDs[*tenant.ParentID] && !excludedIDs[tenant.ID] {
				excludedIDs[tenant.ID] = true
				changed = true
			}
		}
	}

	filtered := make([]domain.Tenant, 0, len(tenants))
	for _, tenant := range tenants {
		if !excludedIDs[tenant.ID] {
			filtered = append(filtered, tenant)
		}
	}
	return filtered, nil
}

func privateTenantRootIDs(tenants []domain.Tenant) []string {
	tenantByID := make(map[string]domain.Tenant, len(tenants))
	for _, tenant := range tenants {
		tenantByID[tenant.ID] = tenant
	}

	roots := make([]string, 0)
	for _, tenant := range tenants {
		if tenantVisibility(tenant.Config) != "private" {
			continue
		}
		if tenant.ParentID != nil {
			parent, ok := tenantByID[*tenant.ParentID]
			if ok && tenantVisibility(parent.Config) == "private" {
				continue
			}
		}
		roots = append(roots, tenant.ID)
	}
	return roots
}

func (h *TenantHandler) canViewPrivateTenant(ctx context.Context, profile *domain.UserProfileResponse, privateRootID string, tenants []domain.Tenant) (bool, error) {
	if profile == nil {
		return false, nil
	}
	if profileCanManageTenantOrAncestor(profile, privateRootID, tenants) {
		return true, nil
	}
	if h.Keto == nil || strings.TrimSpace(profile.ID) == "" {
		return false, nil
	}

	subject := "User:" + profile.ID
	for _, relation := range []string{"view_private", "view_private_descendants", "view", "manage"} {
		allowed, err := h.Keto.CheckPermission(ctx, subject, "Tenant", privateRootID, relation)
		if err != nil {
			return false, fmt.Errorf("private tenant permission check failed: %w", err)
		}
		if allowed {
			return true, nil
		}
	}

	for _, ancestorID := range tenantAncestorIDs(privateRootID, tenants) {
		allowed, err := h.Keto.CheckPermission(ctx, subject, "Tenant", ancestorID, "view_private_descendants")
		if err != nil {
			return false, fmt.Errorf("private tenant descendant permission check failed: %w", err)
		}
		if allowed {
			return true, nil
		}
	}
	return false, nil
}

func profileCanManageTenantOrAncestor(profile *domain.UserProfileResponse, tenantID string, tenants []domain.Tenant) bool {
	manageableIDs := make(map[string]bool, len(profile.ManageableTenants))
	for _, tenant := range profile.ManageableTenants {
		if tenant.ID != "" {
			manageableIDs[tenant.ID] = true
		}
	}
	if len(manageableIDs) == 0 {
		return false
	}
	if manageableIDs[tenantID] {
		return true
	}
	for _, ancestorID := range tenantAncestorIDs(tenantID, tenants) {
		if manageableIDs[ancestorID] {
			return true
		}
	}
	return false
}

func tenantAncestorIDs(tenantID string, tenants []domain.Tenant) []string {
	tenantByID := make(map[string]domain.Tenant, len(tenants))
	for _, tenant := range tenants {
		tenantByID[tenant.ID] = tenant
	}

	ancestors := make([]string, 0)
	visited := map[string]bool{}
	current, ok := tenantByID[tenantID]
	for ok && current.ParentID != nil && *current.ParentID != "" {
		parentID := *current.ParentID
		if visited[parentID] {
			break
		}
		visited[parentID] = true
		ancestors = append(ancestors, parentID)
		current, ok = tenantByID[parentID]
	}
	return ancestors
}

func normalizeTenantUserSchema(value any) ([]any, error) {
	if value == nil {
		return nil, nil
	}

	rawFields, ok := value.([]any)
	if !ok {
		return nil, fmt.Errorf("userSchema must be an array")
	}

	fields := make([]any, 0, len(rawFields))
	for _, raw := range rawFields {
		field, ok := raw.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("userSchema fields must be objects")
		}

		normalized := make(map[string]any, len(field))
		for key, value := range field {
			if key == "maxLength" {
				continue
			}
			normalized[key] = value
		}

		isLoginID, _ := normalized["isLoginId"].(bool)
		if isLoginID {
			fieldType, _ := normalized["type"].(string)
			if fieldType != "" && fieldType != "text" {
				return nil, fmt.Errorf("login ID fields must be text")
			}
			normalized["type"] = "text"
			normalized["indexed"] = true
		} else if indexed, ok := normalized["indexed"].(bool); !ok || !indexed {
			normalized["indexed"] = false
		}

		fields = append(fields, normalized)
	}

	return fields, nil
}

func normalizeTenantDomainForceSet(values []string) map[string]bool {
	domains := normalizeTenantDomainInputs(values)
	force := make(map[string]bool, len(domains))
	for _, domainName := range domains {
		force[domainName] = true
	}
	return force
}

func tenantDomainConflictJSON(c *fiber.Ctx, conflicts []tenantDomainConflict) error {
	return c.Status(fiber.StatusConflict).JSON(fiber.Map{
		"code":      "tenant_domain_conflict",
		"error":     "domain is already assigned to another tenant",
		"conflicts": conflicts,
	})
}

func (h *TenantHandler) findTenantDomainConflicts(ctx context.Context, tenantID string, domains []string, forceDomains []string) ([]tenantDomainConflict, error) {
	if h.DB == nil || h.DB.Config == nil || len(domains) == 0 {
		return nil, nil
	}

	force := normalizeTenantDomainForceSet(forceDomains)
	var rows []domain.TenantDomain
	query := h.DB.WithContext(ctx).Where("domain IN ?", domains)
	if tenantID != "" {
		query = query.Where("tenant_id <> ?", tenantID)
	}
	if err := query.Find(&rows).Error; err != nil {
		return nil, err
	}

	conflicts := make([]tenantDomainConflict, 0, len(rows))
	tenantIDs := make([]string, 0, len(rows))
	seenTenantIDs := make(map[string]bool, len(rows))
	for _, row := range rows {
		if force[row.Domain] {
			continue
		}
		if !seenTenantIDs[row.TenantID] {
			seenTenantIDs[row.TenantID] = true
			tenantIDs = append(tenantIDs, row.TenantID)
		}
	}

	tenantsByID := make(map[string]domain.Tenant, len(tenantIDs))
	if len(tenantIDs) > 0 {
		var tenants []domain.Tenant
		if err := h.DB.WithContext(ctx).Where("id IN ?", tenantIDs).Find(&tenants).Error; err != nil {
			return nil, err
		}
		for _, tenant := range tenants {
			tenantsByID[tenant.ID] = tenant
		}
	}

	for _, row := range rows {
		if force[row.Domain] {
			continue
		}
		conflict := tenantDomainConflict{
			Domain:   row.Domain,
			TenantID: row.TenantID,
		}
		if tenant, ok := tenantsByID[row.TenantID]; ok {
			conflict.TenantName = tenant.Name
			conflict.TenantSlug = tenant.Slug
		}
		conflicts = append(conflicts, conflict)
	}

	return conflicts, nil
}

func (h *TenantHandler) replaceTenantDomains(ctx context.Context, tenantID string, domains []string, forceDomains []string) error {
	if h.DB == nil {
		return errors.New("database not available")
	}
	if h.DB.Config == nil {
		return nil
	}

	deleteQuery := h.DB.WithContext(ctx).Where("tenant_id = ?", tenantID)
	if len(domains) > 0 {
		deleteQuery = deleteQuery.Where("domain NOT IN ?", domains)
	}
	if err := deleteQuery.Delete(&domain.TenantDomain{}).Error; err != nil {
		return fmt.Errorf("failed to clear old domains: %w", err)
	}

	for _, domainName := range domains {
		var existing domain.TenantDomain
		err := h.DB.WithContext(ctx).Unscoped().
			Where("tenant_id = ? AND domain = ?", tenantID, domainName).
			First(&existing).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			if err := repository.NewTenantRepository(h.DB).AddDomain(ctx, tenantID, domainName, true); err != nil {
				return fmt.Errorf("failed to add domain: %s", domainName)
			}
			continue
		}
		if err != nil {
			return err
		}
		if err := h.DB.WithContext(ctx).Unscoped().Model(&existing).Updates(map[string]any{
			"verified":   true,
			"deleted_at": nil,
		}).Error; err != nil {
			return fmt.Errorf("failed to add domain: %s", domainName)
		}
	}
	return nil
}

func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord) (*domain.Tenant, bool, error) {
	if h.DB == nil {
		if record.TenantID != "" {
			return nil, false, errors.New("database not available for tenant update")
		}
		return nil, false, nil
	}

	var tenant domain.Tenant
	query := h.DB.Preload("Domains")
	var err error
	if record.TenantID != "" {
		err = query.First(&tenant, "id = ?", record.TenantID).Error
	} else {
		err = query.First(&tenant, "slug = ?", record.Slug).Error
	}

	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, false, nil
	}
	if err != nil {
		return nil, false, err
	}

	tenant.Name = record.Name
	tenant.Type = record.Type
	tenant.ParentID = record.ParentTenantID
	tenant.Slug = record.Slug
	tenant.Description = record.Memo
	if tenant.Status == "" {
		tenant.Status = domain.TenantStatusActive
	}
	mergedConfig, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record)
	if err != nil {
		return nil, false, err
	}
	if changedConfig {
		tenant.Config = mergedConfig
	}

	if err := h.DB.Save(&tenant).Error; err != nil {
		return nil, false, err
	}
	if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil {
		return nil, false, err
	}
	repo := repository.NewTenantRepository(h.DB)
	for _, domainName := range record.Domains {
		if err := repo.AddDomain(c.Context(), tenant.ID, domainName, true); err != nil {
			return nil, false, err
		}
	}

	return &tenant, true, nil
}

func (h *TenantHandler) createTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord, creatorID string) (*domain.Tenant, error) {
	if h.DB != nil && record.TenantID != "" {
		var exists int64
		if err := h.DB.Unscoped().Model(&domain.Tenant{}).Where("slug = ?", record.Slug).Count(&exists).Error; err != nil {
			return nil, err
		}
		if exists > 0 {
			return nil, errors.New("tenant slug already exists")
		}

		tenant := domain.Tenant{
			ID:          record.TenantID,
			Type:        record.Type,
			ParentID:    record.ParentTenantID,
			Name:        record.Name,
			Slug:        record.Slug,
			Description: record.Memo,
			Status:      domain.TenantStatusActive,
		}
		config, _, err := mergeTenantCSVRecordConfig(nil, record)
		if err != nil {
			return nil, err
		}
		if len(config) > 0 {
			tenant.Config = config
		}
		if err := h.DB.Create(&tenant).Error; err != nil {
			return nil, err
		}
		if h.KetoOutbox != nil {
			_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
				Namespace: "Tenant",
				Object:    tenant.ID,
				Relation:  "admins",
				Subject:   "System:global#super_admins",
				Action:    domain.KetoOutboxActionCreate,
			})
			if tenant.ParentID != nil {
				_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
					Namespace: "Tenant",
					Object:    tenant.ID,
					Relation:  "parents",
					Subject:   "Tenant:" + *tenant.ParentID,
					Action:    domain.KetoOutboxActionCreate,
				})
			}
			if creatorID != "" {
				for _, relation := range []string{"owners", "admins", "members"} {
					_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
						Namespace: "Tenant",
						Object:    tenant.ID,
						Relation:  relation,
						Subject:   "User:" + creatorID,
						Action:    domain.KetoOutboxActionCreate,
					})
				}
			}
		}
		repo := repository.NewTenantRepository(h.DB)
		for _, domainName := range record.Domains {
			if err := repo.AddDomain(c.Context(), tenant.ID, domainName, true); err != nil {
				return nil, err
			}
		}
		return &tenant, nil
	}

	tenant, err := h.Service.RegisterTenant(c.Context(), record.Name, record.Slug, record.Type, record.Memo, record.Domains, record.ParentTenantID, creatorID)
	if err != nil || tenant == nil {
		return tenant, err
	}
	config, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record)
	if err != nil {
		return nil, err
	}
	if changedConfig {
		if h.DB == nil {
			return nil, errors.New("database not available for tenant config import")
		}
		tenant.Config = config
		if err := h.DB.Save(tenant).Error; err != nil {
			return nil, err
		}
	}
	return tenant, err
}

func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
	if h.DB == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
	}

	tenantID := strings.TrimSpace(c.Params("id"))
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	var tenant domain.Tenant
	if err := h.DB.Preload("Domains").First(&tenant, "id = ?", tenantID).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "tenant not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	memberCounts, err := h.countTenantMembersFromProjection(c.Context(), []domain.Tenant{tenant})
	if err != nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
	}

	summary := mapTenantSummary(tenant)
	summary.MemberCount = memberCounts[tenant.ID]

	return c.JSON(summary)
}

func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
	if h.DB == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
	}

	var req struct {
		Name         string         `json:"name"`
		Slug         string         `json:"slug"`
		Type         string         `json:"type"`
		Description  string         `json:"description"`
		Status       string         `json:"status"`
		Domains      []string       `json:"domains"`
		ForceDomains []string       `json:"forceDomainConflicts"`
		ParentID     *string        `json:"parentId"`
		Config       map[string]any `json:"config"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	name := strings.TrimSpace(req.Name)
	if name == "" {
		return errorJSON(c, fiber.StatusBadRequest, "name is required")
	}

	tenantType := normalizeTenantType(req.Type)
	if tenantType == "" {
		tenantType = domain.TenantTypeCompany // Default to COMPANY
	}

	slug := req.Slug
	if slug == "" {
		slug = utils.GenerateUniqueSlug(name, func(s string) bool {
			var count int64
			h.DB.Unscoped().Model(&domain.Tenant{}).Where("slug = ?", s).Count(&count)
			return count > 0
		})
	} else {
		slug = utils.GenerateSlug(slug)
	}
	if slug == "" {
		return errorJSON(c, fiber.StatusBadRequest, "slug is required")
	}

	status := normalizeTenantStatus(req.Status)
	if status == "" {
		status = "active"
	}

	// Use Service
	var parentID *string
	if req.ParentID != nil && strings.TrimSpace(*req.ParentID) != "" {
		pid := strings.TrimSpace(*req.ParentID)
		parentID = &pid
	}

	// Extract creator ID if present
	creatorID := ""
	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok {
		creatorID = profile.ID
	}

	normalizedDomains := normalizeTenantDomainInputs(req.Domains)
	conflicts, err := h.findTenantDomainConflicts(c.Context(), "", normalizedDomains, req.ForceDomains)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if len(conflicts) > 0 {
		return tenantDomainConflictJSON(c, conflicts)
	}

	tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, tenantType, req.Description, nil, parentID, creatorID)
	if err != nil {
		if strings.Contains(err.Error(), "already exists") {
			return errorJSON(c, fiber.StatusConflict, err.Error())
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	summary := mapTenantSummary(*tenant)
	summary.MemberCount = 0

	if req.Config != nil {
		config, err := normalizeTenantConfig(req.Config)
		if err != nil {
			return errorJSON(c, fiber.StatusBadRequest, err.Error())
		}
		var tenants []domain.Tenant
		if hasTenantOrgConfig(config) {
			if err := h.DB.Find(&tenants).Error; err != nil {
				return errorJSON(c, fiber.StatusInternalServerError, err.Error())
			}
			if err := validateTenantOrgConfigScope(*tenant, tenants, config); err != nil {
				return errorJSON(c, fiber.StatusBadRequest, err.Error())
			}
		}
		tenant.Config = config
		h.DB.Save(tenant)
		summary.Config = tenant.Config
	}
	if err := h.replaceTenantDomains(c.Context(), tenant.ID, normalizedDomains, req.ForceDomains); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if len(normalizedDomains) > 0 {
		summary.Domains = normalizedDomains
	}
	if h.Worksmobile != nil {
		if refreshed := h.DB.Preload("Domains").First(tenant, "id = ?", tenant.ID); refreshed.Error == nil {
			if err := h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant); err != nil {
				fmt.Printf("[TenantHandler] failed to enqueue Worksmobile tenant sync: %v\n", err)
			}
		}
	}

	return c.Status(fiber.StatusCreated).JSON(summary)
}

func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
	if h.DB == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
	}

	tenantID := strings.TrimSpace(c.Params("id"))
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	var tenant domain.Tenant
	if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "tenant not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	var req struct {
		Name         *string        `json:"name"`
		Type         *string        `json:"type"`
		Slug         *string        `json:"slug"`
		Description  *string        `json:"description"`
		Status       *string        `json:"status"`
		ParentID     *string        `json:"parentId"`
		Domains      []string       `json:"domains"`
		ForceDomains []string       `json:"forceDomainConflicts"`
		Config       map[string]any `json:"config"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	if req.Name != nil {
		name := strings.TrimSpace(*req.Name)
		if name == "" {
			return errorJSON(c, fiber.StatusBadRequest, "name cannot be empty")
		}
		tenant.Name = name
	}
	if req.Type != nil {
		tenantType := normalizeTenantType(*req.Type)
		if tenantType == "" {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid tenant type"})
		}
		tenant.Type = tenantType
	}
	if req.Slug != nil {
		slug := utils.GenerateSlug(*req.Slug)
		if slug == "" {
			return errorJSON(c, fiber.StatusBadRequest, "slug cannot be empty")
		}
		if slug != tenant.Slug {
			var exists domain.Tenant
			if err := h.DB.Unscoped().Where("slug = ?", slug).First(&exists).Error; err == nil {
				return errorJSON(c, fiber.StatusConflict, "slug already exists")
			} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return errorJSON(c, fiber.StatusInternalServerError, err.Error())
			}
			tenant.Slug = slug
		}
	}
	if req.Description != nil {
		tenant.Description = strings.TrimSpace(*req.Description)
	}
	if req.Status != nil {
		status := normalizeTenantStatus(*req.Status)
		if status == "" {
			return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
		}
		tenant.Status = status
	}
	if req.ParentID != nil {
		pid := strings.TrimSpace(*req.ParentID)
		if pid == "" {
			tenant.ParentID = nil
		} else {
			// 순환 참조(Circular Dependency) 방지 로직:
			// 새로운 부모(pid)부터 상위로 탐색하면서 현재 테넌트(tenant.ID)가 나오면 순환 참조로 간주함
			checkID := pid
			for checkID != "" {
				if checkID == tenant.ID {
					return errorJSON(c, fiber.StatusConflict, "순환 참조 오류: 하위 테넌트를 상위 테넌트로 지정할 수 없습니다.")
				}
				var pTenant domain.Tenant
				if err := h.DB.Select("id, parent_id").First(&pTenant, "id = ?", checkID).Error; err != nil {
					break // 데이터를 찾을 수 없거나 에러 발생 시 반복문 종료 (추후 외래키 제약조건 등에서 에러 발생)
				}
				if pTenant.ParentID != nil {
					checkID = *pTenant.ParentID
				} else {
					break
				}
			}

			tenant.ParentID = &pid
		}

		// [Keto] Sync hierarchy via Outbox
		if h.KetoOutbox != nil {
			if tenant.ParentID != nil {
				_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
					Namespace: "Tenant",
					Object:    tenant.ID,
					Relation:  "parents",
					Subject:   "Tenant:" + *tenant.ParentID,
					Action:    domain.KetoOutboxActionCreate,
				})
			} else {
				// We don't have enough info here to delete specific parent if we don't know the old one,
				// but for now we focus on adding.
			}
		}
	}
	if req.Config != nil {
		config, err := normalizeTenantConfig(req.Config)
		if err != nil {
			return errorJSON(c, fiber.StatusBadRequest, err.Error())
		}
		var tenants []domain.Tenant
		if hasTenantOrgConfig(config) {
			if err := h.DB.Find(&tenants).Error; err != nil {
				return errorJSON(c, fiber.StatusInternalServerError, err.Error())
			}
			if err := validateTenantOrgConfigScope(tenant, tenants, config); err != nil {
				return errorJSON(c, fiber.StatusBadRequest, err.Error())
			}
		}
		tenant.Config = config
	}

	if err := h.DB.Save(&tenant).Error; err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	// Update domains if provided
	if req.Domains != nil {
		normalizedDomains := normalizeTenantDomainInputs(req.Domains)
		conflicts, err := h.findTenantDomainConflicts(c.Context(), tenant.ID, normalizedDomains, req.ForceDomains)
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
		if len(conflicts) > 0 {
			return tenantDomainConflictJSON(c, conflicts)
		}
		if err := h.replaceTenantDomains(c.Context(), tenant.ID, normalizedDomains, req.ForceDomains); err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
	}

	// Refetch to get updated relations
	h.DB.Preload("Domains").First(&tenant, "id = ?", tenant.ID)
	if h.Worksmobile != nil {
		if err := h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), tenant); err != nil {
			fmt.Printf("[TenantHandler] failed to enqueue Worksmobile tenant update sync: %v\n", err)
		}
	}

	return c.JSON(mapTenantSummary(tenant))
}

func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
	if h.DB == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
	}

	tenantID := strings.TrimSpace(c.Params("id"))
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	var tenant domain.Tenant
	if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return errorJSON(c, fiber.StatusNotFound, "tenant not found")
		}
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if bootstrap.IsSeedTenantSlug(tenant.Slug) {
		return seedTenantDeleteError(c)
	}

	// Rename slug to release it for reuse before soft delete
	deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405")
	if err := h.DB.Model(&tenant).Update("slug", deletedSlug).Error; err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, "failed to release slug")
	}

	if err := h.DB.Delete(&tenant).Error; err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	if h.Worksmobile != nil {
		if err := h.Worksmobile.EnqueueTenantDeleteIfInScope(c.Context(), tenant); err != nil {
			fmt.Printf("[TenantHandler] failed to enqueue Worksmobile tenant delete sync: %v\n", err)
		}
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	// Fetch admins from Keto
	relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", "")
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	type adminInfo struct {
		ID    string `json:"id"`
		Name  string `json:"name"`
		Email string `json:"email"`
	}
	admins := []adminInfo{}

	for _, rel := range relations {
		if !strings.HasPrefix(rel.SubjectID, "User:") {
			continue
		}
		userID := strings.TrimPrefix(rel.SubjectID, "User:")

		// Fetch user details - Try Kratos first, then local DB
		name := "Unknown"
		email := "Unknown"

		identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
		if err == nil && identity != nil {
			if n, ok := identity.Traits["name"].(string); ok {
				name = n
			}
			if e, ok := identity.Traits["email"].(string); ok {
				email = e
			}
		} else if h.UserRepo != nil {
			// Fallback to local DB (useful for Mock users or users not yet synced/migrated to Kratos)
			user, err := h.UserRepo.FindByID(c.Context(), userID)
			if err == nil && user != nil {
				name = user.Name
				email = user.Email
			} else if userID == "00000000-0000-0000-0000-000000000000" {
				name = "Dev Mock User"
				email = "mock@hmac.kr"
			}
		}

		admins = append(admins, adminInfo{
			ID:    userID,
			Name:  name,
			Email: email,
		})
	}

	return c.JSON(admins)
}

func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	userID := c.Params("userId")
	if tenantID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
	}

	if h.Keto != nil {
		relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", "User:"+userID)
		if err == nil && len(relations) > 0 {
			return errorJSON(c, fiber.StatusConflict, "이미 관리자로 등록된 사용자입니다.")
		}
	}

	if h.KetoOutbox != nil {
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "admins",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})
		// Also add as member for UI visibility/ReBAC logic
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "members",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})
	}

	return c.SendStatus(fiber.StatusOK)
}

func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	userID := c.Params("userId")
	if tenantID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
	}

	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok {
		if profile.ID == userID {
			return errorJSON(c, fiber.StatusBadRequest, "cannot remove yourself from admin role")
		}
	}

	if h.Keto != nil {
		if relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", ""); err == nil {
			adminCount := 0
			isTargetAdmin := false
			for _, rel := range relations {
				if strings.HasPrefix(rel.SubjectID, "User:") {
					adminCount++
					if rel.SubjectID == "User:"+userID {
						isTargetAdmin = true
					}
				}
			}
			if isTargetAdmin && adminCount <= 1 {
				return errorJSON(c, fiber.StatusBadRequest, "cannot remove the last admin")
			}
		}
	}

	if h.KetoOutbox != nil {
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "admins",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionDelete,
		})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *TenantHandler) ListOwners(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	if tenantID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
	}

	// Fetch owners from Keto
	relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", "")
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	type ownerInfo struct {
		ID    string `json:"id"`
		Name  string `json:"name"`
		Email string `json:"email"`
	}
	owners := []ownerInfo{}

	for _, rel := range relations {
		if !strings.HasPrefix(rel.SubjectID, "User:") {
			continue
		}
		userID := strings.TrimPrefix(rel.SubjectID, "User:")

		// Fetch user details - Try Kratos first, then local DB
		name := "Unknown"
		email := "Unknown"

		identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
		if err == nil && identity != nil {
			if n, ok := identity.Traits["name"].(string); ok {
				name = n
			}
			if e, ok := identity.Traits["email"].(string); ok {
				email = e
			}
		} else if h.UserRepo != nil {
			// Fallback to local DB
			user, err := h.UserRepo.FindByID(c.Context(), userID)
			if err == nil && user != nil {
				name = user.Name
				email = user.Email
			} else if userID == "00000000-0000-0000-0000-000000000000" {
				name = "Dev Mock User"
				email = "mock@hmac.kr"
			}
		}

		owners = append(owners, ownerInfo{
			ID:    userID,
			Name:  name,
			Email: email,
		})
	}

	return c.JSON(owners)
}

func (h *TenantHandler) AddOwner(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	userID := c.Params("userId")
	if tenantID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
	}

	if h.Keto != nil {
		relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", "User:"+userID)
		if err == nil && len(relations) > 0 {
			return errorJSON(c, fiber.StatusConflict, "이미 소유자로 등록된 사용자입니다.")
		}
	}

	if h.KetoOutbox != nil {
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "owners",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})
		// Also add as member for UI visibility/ReBAC logic
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "members",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionCreate,
		})
	}

	return c.SendStatus(fiber.StatusOK)
}

func (h *TenantHandler) RemoveOwner(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	userID := c.Params("userId")
	if tenantID == "" || userID == "" {
		return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
	}

	if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok {
		if profile.ID == userID {
			return errorJSON(c, fiber.StatusBadRequest, "cannot remove yourself from owner role")
		}
	}

	if h.Keto != nil {
		if relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", ""); err == nil {
			ownerCount := 0
			isTargetOwner := false
			for _, rel := range relations {
				if strings.HasPrefix(rel.SubjectID, "User:") {
					ownerCount++
					if rel.SubjectID == "User:"+userID {
						isTargetOwner = true
					}
				}
			}
			if isTargetOwner && ownerCount <= 1 {
				return errorJSON(c, fiber.StatusBadRequest, "cannot remove the last owner")
			}
		}
	}

	if h.KetoOutbox != nil {
		_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenantID,
			Relation:  "owners",
			Subject:   "User:" + userID,
			Action:    domain.KetoOutboxActionDelete,
		})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func (h *TenantHandler) DeleteTenantsBulk(c *fiber.Ctx) error {
	var req struct {
		IDs []string `json:"ids"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	if len(req.IDs) == 0 {
		return errorJSON(c, fiber.StatusBadRequest, "no IDs provided")
	}

	// Permission check: Super Admin can delete anything.
	// Tenant Admin should theoretically only delete manageable sub-tenants,
	// but currently bulk delete is intended for Super Admin.
	profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
	if profile == nil || domain.NormalizeRole(profile.Role) != domain.RoleSuperAdmin {
		return errorJSON(c, fiber.StatusForbidden, "only super admin can perform bulk deletion")
	}

	protectedSlugs := seedTenantSlugsForDeleteGuard()
	if len(protectedSlugs) > 0 {
		var protectedCount int64
		if err := h.DB.Model(&domain.Tenant{}).
			Where("id IN ?", req.IDs).
			Where("slug IN ?", protectedSlugs).
			Count(&protectedCount).Error; err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
		if protectedCount > 0 {
			return seedTenantDeleteError(c)
		}
	}

	if err := h.Service.DeleteTenantsBulk(c.Context(), req.IDs); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"message": "Tenants deleted successfully",
		"count":   len(req.IDs),
	})
}

func mapTenantSummary(t domain.Tenant) tenantSummary {
	domains := make([]string, 0, len(t.Domains))
	for _, d := range t.Domains {
		domains = append(domains, d.Domain)
	}

	return tenantSummary{
		ID:          t.ID,
		Type:        t.Type,
		ParentID:    t.ParentID,
		Name:        t.Name,
		Slug:        t.Slug,
		Description: t.Description,
		Status:      t.Status,
		Domains:     domains,
		Config:      t.Config,
		CreatedAt:   t.CreatedAt.Format(time.RFC3339),
		UpdatedAt:   t.UpdatedAt.Format(time.RFC3339),
	}
}

func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
	if c.Locals("apiKeyName") == nil {
		return errorJSON(c, fiber.StatusUnauthorized, "api key authentication is required")
	}
	if h.Service == nil {
		return errorJSON(c, fiber.StatusServiceUnavailable, "tenant service is not configured")
	}

	allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	rootSlug := strings.TrimSpace(c.Query("tenantSlug"))
	if rootSlug == "" {
		rootSlug = "hanmac-family"
	}

	root, ok := findOrgContextTenantBySlug(allTenants, rootSlug)
	if !ok {
		return errorJSON(c, fiber.StatusNotFound, "tenant slug not found")
	}

	scopedTenants := filterOrgContextSubtree(allTenants, root.ID)
	contextTenants := make([]orgContextTenant, 0, len(scopedTenants))
	tenantIDs := make([]string, 0, len(scopedTenants))
	tenantSlugs := make([]string, 0, len(scopedTenants))
	tenantByID := make(map[string]orgContextTenant, len(scopedTenants))
	tenantBySlug := make(map[string]orgContextTenant, len(scopedTenants))
	for _, tenant := range scopedTenants {
		summary := mapOrgContextTenant(tenant)
		contextTenants = append(contextTenants, summary)
		tenantIDs = append(tenantIDs, tenant.ID)
		tenantSlugs = append(tenantSlugs, tenant.Slug)
		tenantByID[tenant.ID] = summary
		tenantBySlug[strings.ToLower(tenant.Slug)] = summary
	}

	includeUsers := !strings.EqualFold(strings.TrimSpace(c.Query("includeUsers")), "false")
	includeUserIDs := strings.EqualFold(strings.TrimSpace(c.Query("includeUserIds")), "true")
	membersByTenantID := make(map[string][]orgContextMember)
	if includeUsers {
		if h.UserRepo == nil {
			return errorJSON(c, fiber.StatusServiceUnavailable, "user repository is not configured")
		}
		membersByTenantID, err = h.loadOrgContextMembers(c.Context(), tenantIDs, tenantSlugs, tenantByID, tenantBySlug, includeUserIDs)
		if err != nil {
			return errorJSON(c, fiber.StatusInternalServerError, err.Error())
		}
	}
	for i := range contextTenants {
		members := membersByTenantID[contextTenants[i].ID]
		if members == nil {
			members = []orgContextMember{}
		}
		contextTenants[i].Members = members
		tenantByID[contextTenants[i].ID] = contextTenants[i]
		tenantBySlug[strings.ToLower(contextTenants[i].Slug)] = contextTenants[i]
	}

	tree := buildOrgContextTree(root.ID, scopedTenants, tenantByID)
	return c.JSON(orgContextResponse{
		SchemaVersion: "baron.org-context.v1",
		IssuedAt:      time.Now().UTC().Format(time.RFC3339),
		Scope: orgContextScope{
			TenantID:   root.ID,
			TenantSlug: root.Slug,
		},
		Tree:    tree,
		Tenants: contextTenants,
	})
}

func (h *TenantHandler) loadOrgContextMembers(ctx context.Context, tenantIDs, tenantSlugs []string, tenantByID, tenantBySlug map[string]orgContextTenant, includeUserIDs bool) (map[string][]orgContextMember, error) {
	usersByID, err := h.UserRepo.FindByTenantIDs(ctx, tenantIDs)
	if err != nil {
		return nil, err
	}
	usersBySlug, err := h.UserRepo.FindByCompanyCodes(ctx, tenantSlugs)
	if err != nil {
		return nil, err
	}
	usersByAppointment, _, err := h.UserRepo.List(ctx, 0, 10000, "", "")
	if err != nil {
		return nil, err
	}

	seen := make(map[string]bool)
	membersByTenantID := make(map[string][]orgContextMember)
	users := append(usersByID, usersBySlug...)
	users = append(users, usersByAppointment...)
	for _, user := range users {
		if seen[user.ID] || !domain.IsOrgVisibleUserStatus(user.Status) {
			continue
		}
		assignments := mapOrgContextMemberAssignments(user, tenantByID, tenantBySlug, includeUserIDs)
		if len(assignments) == 0 {
			continue
		}
		seen[user.ID] = true
		for _, assignment := range assignments {
			membersByTenantID[assignment.TenantID] = append(membersByTenantID[assignment.TenantID], assignment.Member)
		}
	}
	return membersByTenantID, nil
}

func findOrgContextTenantBySlug(tenants []domain.Tenant, slug string) (domain.Tenant, bool) {
	normalized := strings.ToLower(strings.TrimSpace(slug))
	for _, tenant := range tenants {
		if strings.ToLower(tenant.Slug) == normalized && isOrgContextTenantType(tenant) {
			return tenant, true
		}
	}
	return domain.Tenant{}, false
}

func isOrgContextTenantType(tenant domain.Tenant) bool {
	switch strings.ToUpper(tenant.Type) {
	case domain.TenantTypeCompanyGroup, domain.TenantTypeCompany, domain.TenantTypeOrganization, domain.TenantTypeUserGroup:
		return true
	default:
		return false
	}
}

func filterOrgContextSubtree(tenants []domain.Tenant, rootID string) []domain.Tenant {
	descendantIDs := map[string]bool{rootID: true}
	frontier := map[string]bool{rootID: true}
	for len(frontier) > 0 {
		next := map[string]bool{}
		for _, tenant := range tenants {
			if tenant.ParentID == nil || !frontier[*tenant.ParentID] || descendantIDs[tenant.ID] {
				continue
			}
			descendantIDs[tenant.ID] = true
			next[tenant.ID] = true
		}
		frontier = next
	}

	excludedIDs := map[string]bool{}
	for _, tenant := range tenants {
		if descendantIDs[tenant.ID] && tenantVisibility(tenant.Config) == "private" {
			excludedIDs[tenant.ID] = true
		}
	}
	changed := true
	for changed {
		changed = false
		for _, tenant := range tenants {
			if tenant.ParentID == nil || !descendantIDs[tenant.ID] || excludedIDs[tenant.ID] {
				continue
			}
			if excludedIDs[*tenant.ParentID] {
				excludedIDs[tenant.ID] = true
				changed = true
			}
		}
	}

	filtered := make([]domain.Tenant, 0, len(descendantIDs))
	for _, tenant := range tenants {
		if descendantIDs[tenant.ID] && !excludedIDs[tenant.ID] && isOrgContextTenantType(tenant) {
			filtered = append(filtered, tenant)
		}
	}
	return filtered
}

func mapOrgContextTenant(tenant domain.Tenant) orgContextTenant {
	domains := make([]string, 0, len(tenant.Domains))
	for _, domain := range tenant.Domains {
		domains = append(domains, domain.Domain)
	}
	visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
	return orgContextTenant{
		ID:          tenant.ID,
		Type:        tenant.Type,
		Name:        tenant.Name,
		Slug:        tenant.Slug,
		ParentID:    tenant.ParentID,
		Status:      tenant.Status,
		Description: tenant.Description,
		Domains:     domains,
		Visibility:  visibility,
		OrgUnitType: orgUnitType,
		Config:      tenant.Config,
		CreatedAt:   tenant.CreatedAt.Format(time.RFC3339),
		UpdatedAt:   tenant.UpdatedAt.Format(time.RFC3339),
		Members:     []orgContextMember{},
	}
}

func mapOrgContextMemberAssignments(user domain.User, tenantByID, tenantBySlug map[string]orgContextTenant, includeUserIDs bool) []orgContextMemberAssignment {
	assignments := make([]orgContextMemberAssignment, 0, 2)
	seenTenants := map[string]bool{}
	appointments := tenantClaimAppointmentsFromTraits(map[string]any(user.Metadata))

	addTenant := func(tenant orgContextTenant, ok bool, appointment map[string]any) {
		if !ok || seenTenants[tenant.ID] {
			return
		}
		seenTenants[tenant.ID] = true
		if appointment == nil {
			appointment = lookupTenantClaimAppointment(appointments, tenant.ID, &domain.Tenant{
				ID:   tenant.ID,
				Slug: tenant.Slug,
			})
		}
		assignments = append(assignments, orgContextMemberAssignment{
			TenantID: tenant.ID,
			Member:   mapOrgContextMember(user, appointment, includeUserIDs),
		})
	}

	for _, appointment := range appointments {
		for _, key := range []string{"tenantId", "tenant_id"} {
			if tenantID := tenantClaimString(appointment, key); tenantID != "" {
				addTenant(tenantByID[tenantID], tenantByID[tenantID].ID != "", appointment)
			}
		}
		for _, key := range []string{"tenantSlug", "tenant_slug"} {
			if tenantSlug := tenantClaimString(appointment, key); tenantSlug != "" {
				tenant := tenantBySlug[strings.ToLower(tenantSlug)]
				addTenant(tenant, tenant.ID != "", appointment)
			}
		}
	}

	if user.TenantID != nil {
		addTenant(tenantByID[*user.TenantID], tenantByID[*user.TenantID].ID != "", nil)
	}
	if user.Tenant != nil {
		addTenant(tenantByID[user.Tenant.ID], tenantByID[user.Tenant.ID].ID != "", nil)
		tenant := tenantBySlug[strings.ToLower(user.Tenant.Slug)]
		addTenant(tenant, tenant.ID != "", nil)
	}
	return assignments
}

func mapOrgContextMember(user domain.User, appointment map[string]any, includeUserIDs bool) orgContextMember {
	grade := user.Grade
	position := user.Position
	jobTitle := user.JobTitle
	department := user.Department
	if value := tenantClaimString(appointment, "grade"); value != "" {
		grade = value
	}
	if value := tenantClaimString(appointment, "position"); value != "" {
		position = value
	}
	if value := tenantClaimString(appointment, "jobTitle"); value != "" {
		jobTitle = value
	}
	if value := tenantClaimString(appointment, "job_title"); value != "" {
		jobTitle = value
	}
	if value := tenantClaimString(appointment, "department"); value != "" {
		department = value
	}
	isOwner := false
	if value, ok := metadataBoolFromMap(appointment, "isOwner", "isManager"); ok {
		isOwner = value
	}
	isLeader := isOwner
	if value, ok := metadataBoolFromMap(appointment, "lead", "isLead"); ok {
		isLeader = value
	}
	isPrimary := false
	if value, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok {
		isPrimary = value
	}
	id := ""
	phone := ""
	if includeUserIDs {
		id = user.ID
		phone = user.Phone
	}
	return orgContextMember{
		ID:         id,
		Email:      user.Email,
		Name:       user.Name,
		Phone:      phone,
		Department: department,
		Grade:      grade,
		Position:   position,
		JobTitle:   jobTitle,
		IsOwner:    isOwner,
		IsLeader:   isLeader,
		IsPrimary:  isPrimary,
	}
}

func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[string]orgContextTenant) *orgContextTreeNode {
	childrenByParentID := make(map[string][]domain.Tenant)
	for _, tenant := range tenants {
		if tenant.ParentID == nil {
			continue
		}
		childrenByParentID[*tenant.ParentID] = append(childrenByParentID[*tenant.ParentID], tenant)
	}

	var build func(tenantID string) *orgContextTreeNode
	build = func(tenantID string) *orgContextTreeNode {
		tenant, ok := tenantByID[tenantID]
		if !ok {
			return nil
		}
		node := &orgContextTreeNode{
			orgContextTenant: tenant,
			Children:         []orgContextTreeNode{},
		}
		for _, child := range childrenByParentID[tenantID] {
			childNode := build(child.ID)
			if childNode != nil {
				node.Children = append(node.Children, *childNode)
			}
		}
		return node
	}
	return build(rootID)
}

func (h *TenantHandler) countTenantMembersFromProjection(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
	counts := make(map[string]int64, len(tenants))
	for _, tenant := range tenants {
		counts[tenant.ID] = 0
	}
	if len(tenants) == 0 {
		return counts, nil
	}
	if h.UserProjectionRepo == nil {
		return nil, errors.New("user projection is not configured")
	}
	ready, err := h.UserProjectionRepo.IsReady(ctx)
	if err != nil {
		return nil, fmt.Errorf("user projection status unavailable: %w", err)
	}
	if !ready {
		return nil, errors.New("user projection is not ready")
	}
	return h.UserProjectionRepo.CountTenantMembers(ctx, tenants)
}

func normalizeTenantStatus(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "" {
		return ""
	}
	if value != "active" && value != "inactive" {
		return ""
	}
	return value
}

func normalizeTenantType(value string) string {
	value = strings.ToUpper(strings.TrimSpace(value))
	switch value {
	case domain.TenantTypePersonal, domain.TenantTypeCompany, domain.TenantTypeCompanyGroup, domain.TenantTypeOrganization, domain.TenantTypeUserGroup:
		return value
	default:
		return ""
	}
}

func (h *TenantHandler) CreateShareLink(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	var req struct {
		Name        string     `json:"name"`
		Description string     `json:"description"`
		ExpiresAt   *time.Time `json:"expiresAt"`
	}
	if err := c.BodyParser(&req); err != nil {
		return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
	}

	link, err := h.SharedLink.CreateLink(c.Context(), tenantID, req.Name, req.Description, req.ExpiresAt)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	return c.JSON(link)
}

func (h *TenantHandler) ListShareLinks(c *fiber.Ctx) error {
	tenantID := c.Params("id")
	links, err := h.SharedLink.GetLinksByTenant(c.Context(), tenantID)
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	return c.JSON(links)
}

func (h *TenantHandler) DeleteShareLink(c *fiber.Ctx) error {
	id := c.Params("id")
	if err := h.SharedLink.DeactivateLink(c.Context(), id); err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}
	return c.JSON(fiber.Map{"message": "Share link deleted successfully"})
}

func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
	token := c.Query("token")
	if token == "" {
		return errorJSON(c, fiber.StatusUnauthorized, "share token is required")
	}

	link, err := h.SharedLink.ValidateToken(c.Context(), token)
	if err != nil {
		return errorJSON(c, fiber.StatusUnauthorized, err.Error())
	}

	allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
	if err != nil {
		return errorJSON(c, fiber.StatusInternalServerError, err.Error())
	}

	parentMap := make(map[string]string)
	for _, t := range allTenants {
		if t.ParentID != nil {
			parentMap[t.ID] = *t.ParentID
		}
	}

	findRoot := func(id string) string {
		curr := id
		for {
			p, exists := parentMap[curr]
			if !exists || p == "" {
				break
			}
			curr = p
		}
		return curr
	}

	sharedRootID := findRoot(link.TenantID)
	var filteredTenants []domain.Tenant
	var tenantIDs []string

	for _, t := range allTenants {
		if findRoot(t.ID) == sharedRootID {
			filteredTenants = append(filteredTenants, t)
		}
	}
	filteredTenants = filterPublicTenants(filteredTenants)
	for _, t := range filteredTenants {
		tenantIDs = append(tenantIDs, t.ID)
	}

	type publicUserSummary struct {
		ID         string `json:"id"`
		Name       string `json:"name"`
		Position   string `json:"position"`
		JobTitle   string `json:"jobTitle"`
		TenantSlug string `json:"tenantSlug"`
		Status     string `json:"status"`
	}

	var publicUsers []publicUserSummary
	seen := make(map[string]bool)

	// Fetch users by IDs
	var usersByID []domain.User
	h.DB.Where("tenant_id IN ?", tenantIDs).Preload("Tenant").Find(&usersByID)
	for _, u := range usersByID {
		if u.Status != "active" || seen[u.ID] {
			continue
		}
		seen[u.ID] = true
		tenantSlug := ""
		if u.Tenant != nil {
			tenantSlug = u.Tenant.Slug
		}
		publicUsers = append(publicUsers, publicUserSummary{
			ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, TenantSlug: tenantSlug, Status: u.Status,
		})
	}

	tenantSummaries := make([]tenantSummary, 0, len(filteredTenants))
	for _, t := range filteredTenants {
		tenantSummaries = append(tenantSummaries, mapTenantSummary(t))
	}

	return c.JSON(fiber.Map{
		"tenants":    tenantSummaries,
		"users":      publicUsers,
		"sharedWith": link.Name,
	})
}
