package bootstrap

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"baron-sso-backend/internal/utils"
	"bytes"
	"context"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
)

const seedTenantCSVPathEnv = "SEED_TENANT_CSV_PATH"

var seedTenantCSVPathCandidates = []string{
	"adminfront/seed-tenant.csv",
	"../adminfront/seed-tenant.csv",
	"../../adminfront/seed-tenant.csv",
	"../../../adminfront/seed-tenant.csv",
	"/app/adminfront/seed-tenant.csv",
}

type InitialTenantConfig struct {
	TenantID    string
	Name        string
	Slug        string
	Type        string
	ParentSlug  string
	Description string
	Domains     []string
	Config      domain.JSONMap
}

func SeedTenants(db *gorm.DB) error {
	slog.Info("[Bootstrap] Checking initial tenant seed...")

	configs, err := loadSeedTenantConfigs()
	if err != nil {
		return err
	}
	if len(configs) == 0 {
		return errors.New("seed tenant csv has no tenant rows")
	}

	if err := syncExistingSeedTenantConfigs(db, configs); err != nil {
		return err
	}

	existingSlugs, existingIDs, err := loadExistingTenantIdentitySet(db)
	if err != nil {
		return err
	}

	missingConfigs := filterMissingSeedTenantConfigs(configs, existingSlugs, existingIDs)
	if len(missingConfigs) == 0 {
		slog.Info("[Bootstrap] Tenant seed skipped because all seed slugs already exist", "count", len(configs))
		return nil
	}

	slog.Info(
		"[Bootstrap] Tenant seed will create missing seed tenants",
		"total", len(configs),
		"missing", len(missingConfigs),
		"existing", len(configs)-len(missingConfigs),
	)
	return seedTenantConfigs(db, missingConfigs)
}

func syncExistingSeedTenantConfigs(db *gorm.DB, configs []InitialTenantConfig) error {
	for _, config := range configs {
		id := strings.TrimSpace(config.TenantID)
		if id == "" {
			continue
		}

		targetSlug := strings.TrimSpace(strings.ToLower(config.Slug))
		if targetSlug == "" {
			continue
		}

		if err := db.Transaction(func(tx *gorm.DB) error {
			var tenant domain.Tenant
			if err := tx.First(&tenant, "id = ?", id).Error; err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return nil
				}
				return fmt.Errorf("load existing seed tenant %q: %w", id, err)
			}

			if strings.TrimSpace(strings.ToLower(tenant.Slug)) == targetSlug {
				return nil
			}

			var conflict domain.Tenant
			err := tx.Select("id").
				Where("LOWER(TRIM(slug)) = ? AND id <> ?", targetSlug, tenant.ID).
				First(&conflict).Error
			if err == nil {
				return fmt.Errorf("seed tenant slug %q for id %q conflicts with tenant id %q", targetSlug, id, conflict.ID)
			}
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return fmt.Errorf("check seed tenant slug conflict %q: %w", targetSlug, err)
			}

			suffix := "-deleted-" + strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
			if err := tx.Unscoped().
				Model(&domain.Tenant{}).
				Where("slug = ? AND id <> ? AND deleted_at IS NOT NULL", targetSlug, tenant.ID).
				Update("slug", gorm.Expr("slug || ?", suffix)).Error; err != nil {
				return fmt.Errorf("rename deleted tenant slug %q before seed repair: %w", targetSlug, err)
			}

			slog.Info(
				"[Bootstrap] Repairing existing seed tenant slug",
				"id", tenant.ID,
				"oldSlug", tenant.Slug,
				"newSlug", targetSlug,
			)
			if err := tx.Model(&domain.Tenant{}).
				Where("id = ?", tenant.ID).
				Update("slug", targetSlug).Error; err != nil {
				return fmt.Errorf("repair seed tenant slug %q for id %q: %w", targetSlug, id, err)
			}
			return nil
		}); err != nil {
			return err
		}
	}
	return nil
}

func loadExistingTenantIdentitySet(db *gorm.DB) (map[string]bool, map[string]bool, error) {
	var tenants []domain.Tenant
	if err := db.Select("id", "slug").Find(&tenants).Error; err != nil {
		return nil, nil, fmt.Errorf("load existing tenants before seed: %w", err)
	}

	slugs := make(map[string]bool, len(tenants))
	ids := make(map[string]bool, len(tenants))
	for _, tenant := range tenants {
		slug := strings.TrimSpace(strings.ToLower(tenant.Slug))
		if slug != "" {
			slugs[slug] = true
		}
		id := strings.TrimSpace(strings.ToLower(tenant.ID))
		if id != "" {
			ids[id] = true
		}
	}
	return slugs, ids, nil
}

func filterMissingSeedTenantConfigs(configs []InitialTenantConfig, existingSlugs map[string]bool, existingIDs map[string]bool) []InitialTenantConfig {
	filtered := make([]InitialTenantConfig, 0, len(configs))
	for _, config := range configs {
		slug := strings.TrimSpace(strings.ToLower(config.Slug))
		id := strings.TrimSpace(strings.ToLower(config.TenantID))
		if slug == "" || existingSlugs[slug] || (id != "" && existingIDs[id]) {
			continue
		}
		filtered = append(filtered, config)
		existingSlugs[slug] = true
		if id != "" {
			existingIDs[id] = true
		}
	}
	return filtered
}

func seedTenantConfigs(db *gorm.DB, configs []InitialTenantConfig) error {
	slog.Info("[Bootstrap] Seeding initial tenants from CSV...", "count", len(configs))
	repo := repository.NewTenantRepository(db)
	userRepo := repository.NewUserRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	outboxRepo := repository.NewKetoOutboxRepository(db)
	svc := service.NewTenantService(repo, userRepo, userGroupRepo, outboxRepo)
	ctx := context.Background()

	for _, config := range orderSeedTenantConfigsByParentSlug(configs) {
		tenantType := config.Type
		if tenantType == "" {
			tenantType = domain.TenantTypeCompany
		}

		var parentID *string
		if config.ParentSlug != "" {
			parent, err := repo.FindBySlug(ctx, config.ParentSlug)
			if err != nil || parent == nil {
				if err == nil {
					err = errors.New("parent tenant not found")
				}
				slog.Error("Failed to resolve parent tenant for seed", "slug", config.Slug, "parentSlug", config.ParentSlug, "error", err)
				return fmt.Errorf("resolve parent tenant %q for seed %q: %w", config.ParentSlug, config.Slug, err)
			}
			parentID = &parent.ID
		}

		slog.Info("[Bootstrap] Creating seed tenant", "name", config.Name, "slug", config.Slug)
		var tenant *domain.Tenant
		var err error
		if config.TenantID != "" {
			tenant, err = createSeedTenant(ctx, repo, outboxRepo, config, tenantType, parentID)
		} else {
			tenant, err = svc.RegisterTenant(ctx, config.Name, config.Slug, tenantType, config.Description, config.Domains, parentID, "")
		}
		if err != nil {
			slog.Error("Failed to seed tenant", "slug", config.Slug, "error", err)
			return err
		}
		tenant.Status = domain.TenantStatusActive
		if len(config.Config) > 0 {
			tenant.Config = config.Config
		}
		if err := db.Save(tenant).Error; err != nil {
			return err
		}
	}
	return nil
}

func loadSeedTenantConfigs() ([]InitialTenantConfig, error) {
	path, err := findSeedTenantCSVPath()
	if err != nil {
		return nil, err
	}

	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open seed tenant csv %q: %w", path, err)
	}
	defer file.Close()

	configs, err := parseSeedTenantCSV(file)
	if err != nil {
		return nil, fmt.Errorf("parse seed tenant csv %q: %w", path, err)
	}
	return configs, nil
}

func SeedTenantSlugSet() (map[string]bool, error) {
	configs, err := loadSeedTenantConfigs()
	if err != nil {
		return nil, err
	}

	slugs := make(map[string]bool, len(configs))
	for _, config := range configs {
		slug := strings.TrimSpace(strings.ToLower(config.Slug))
		if slug != "" {
			slugs[slug] = true
		}
	}
	return slugs, nil
}

func IsSeedTenantSlug(slug string) bool {
	normalized := strings.TrimSpace(strings.ToLower(slug))
	if normalized == "" {
		return false
	}

	slugs, err := SeedTenantSlugSet()
	if err != nil {
		slog.Warn("[Bootstrap] Failed to load seed tenant slug set", "error", err)
		return false
	}
	return slugs[normalized]
}

func findSeedTenantCSVPath() (string, error) {
	if configured := strings.TrimSpace(os.Getenv(seedTenantCSVPathEnv)); configured != "" {
		return configured, nil
	}

	for _, candidate := range seedTenantCSVPathCandidates {
		cleaned := filepath.Clean(candidate)
		if _, err := os.Stat(cleaned); err == nil {
			return cleaned, nil
		}
	}

	return "", fmt.Errorf("seed tenant csv not found; set %s or add adminfront/seed-tenant.csv", seedTenantCSVPathEnv)
}

func parseSeedTenantCSV(r io.Reader) ([]InitialTenantConfig, 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 := seedTenantCSVHeaderIndex(rows[0])
	for _, key := range []string{"name", "type", "slug"} {
		if _, ok := header[key]; !ok {
			return nil, fmt.Errorf("missing required column: %s", key)
		}
	}

	configs := make([]InitialTenantConfig, 0, len(rows)-1)
	for i, row := range rows[1:] {
		if seedTenantCSVRowIsEmpty(row) {
			continue
		}

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

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

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

		config, err := seedTenantCSVRecordConfig(row, header)
		if err != nil {
			return nil, fmt.Errorf("row %d: %w", i+2, err)
		}

		configs = append(configs, InitialTenantConfig{
			TenantID:    seedTenantCSVValue(row, header, "tenant_id"),
			Name:        name,
			Type:        tenantType,
			ParentSlug:  seedTenantCSVValue(row, header, "parent_tenant_slug"),
			Slug:        slug,
			Description: seedTenantCSVValue(row, header, "memo"),
			Domains:     splitSeedTenantCSVDomains(seedTenantCSVValue(row, header, "email_domain")),
			Config:      config,
		})
	}

	return configs, nil
}

func seedTenantCSVHeaderIndex(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",
		"parenttenantslug":   "parent_tenant_slug",
		"parent_tenant_slug": "parent_tenant_slug",
		"parent_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",
		"org_unit_type":      "org_unit_type",
		"orgunittype":        "org_unit_type",
		"organization_type":  "org_unit_type",
		"organizationtype":   "org_unit_type",
		"worksmobile":        "worksmobile_sync",
		"worksmobilesync":    "worksmobile_sync",
		"worksmobile_sync":   "worksmobile_sync",
		"works_sync":         "worksmobile_sync",
		"works":              "worksmobile_sync",
	}
	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 seedTenantCSVValue(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 seedTenantCSVRecordConfig(row []string, header map[string]int) (domain.JSONMap, error) {
	config := domain.JSONMap{}
	visibility := strings.TrimSpace(seedTenantCSVValue(row, header, "visibility"))
	if visibility != "" {
		normalizedVisibility, err := normalizeSeedTenantVisibility(visibility)
		if err != nil {
			return nil, err
		}
		config["visibility"] = normalizedVisibility
	}
	orgUnitType := strings.TrimSpace(seedTenantCSVValue(row, header, "org_unit_type"))
	if orgUnitType != "" {
		if !isAllowedSeedTenantOrgUnitType(orgUnitType) {
			return nil, errors.New("orgUnitType must be one of 실, 팀, TF, TF팀, 센터, 디비전, 셀, 본부, 지역본부, 부, 임원직속")
		}
		config["orgUnitType"] = orgUnitType
	}
	if worksmobileSync := strings.TrimSpace(seedTenantCSVValue(row, header, "worksmobile_sync")); worksmobileSync != "" {
		excluded, err := normalizeSeedTenantWorksmobileExcluded(worksmobileSync)
		if err != nil {
			return nil, err
		}
		config["worksmobileExcluded"] = excluded
	}
	if len(config) == 0 {
		return nil, nil
	}
	return config, nil
}

func normalizeSeedTenantWorksmobileExcluded(value string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", "yes", "y", "true", "1", "on", "sync", "linked", "연동":
		return false, nil
	case "no", "n", "false", "0", "off", "none", "excluded", "exclude", "not_sync", "not-synced", "미연동", "연동안함", "제외":
		return true, nil
	default:
		return false, errors.New("worksmobile_sync must be yes or no")
	}
}

func normalizeSeedTenantVisibility(value string) (string, error) {
	visibility := strings.ToLower(strings.TrimSpace(value))
	if visibility == "" || visibility == "public" {
		return "public", nil
	}
	if visibility != "internal" && visibility != "private" {
		return "", errors.New("visibility must be public, internal, or private")
	}
	return visibility, nil
}

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

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

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

func splitSeedTenantCSVDomains(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 orderSeedTenantConfigsByParentSlug(configs []InitialTenantConfig) []InitialTenantConfig {
	bySlug := make(map[string]InitialTenantConfig, len(configs))
	for _, config := range configs {
		bySlug[strings.ToLower(config.Slug)] = config
	}

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

	for _, config := range configs {
		visit(config)
	}
	return ordered
}

func createSeedTenant(
	ctx context.Context,
	repo repository.TenantRepository,
	outboxRepo repository.KetoOutboxRepository,
	config InitialTenantConfig,
	tenantType string,
	parentID *string,
) (*domain.Tenant, error) {
	tenant := &domain.Tenant{
		ID:          config.TenantID,
		Type:        tenantType,
		Name:        config.Name,
		Slug:        config.Slug,
		Description: config.Description,
		Status:      domain.TenantStatusActive,
		ParentID:    parentID,
		Config:      config.Config,
	}

	if err := repo.Create(ctx, tenant); err != nil {
		return nil, err
	}
	if err := outboxRepo.Create(ctx, &domain.KetoOutbox{
		Namespace: "Tenant",
		Object:    tenant.ID,
		Relation:  "admins",
		Subject:   "System:global#super_admins",
		Action:    domain.KetoOutboxActionCreate,
	}); err != nil {
		return nil, err
	}
	if tenant.ParentID != nil {
		if err := outboxRepo.Create(ctx, &domain.KetoOutbox{
			Namespace: "Tenant",
			Object:    tenant.ID,
			Relation:  "parents",
			Subject:   "Tenant:" + *tenant.ParentID,
			Action:    domain.KetoOutboxActionCreate,
		}); err != nil {
			return nil, err
		}
	}
	for _, domainName := range config.Domains {
		if err := repo.AddDomain(ctx, tenant.ID, domainName, true); err != nil {
			slog.Error("Failed to add domain to seeded tenant", "tenant", config.Slug, "domain", domainName, "error", err)
		}
	}

	return repo.FindBySlug(ctx, config.Slug)
}
