package repository

import (
	"baron-sso-backend/internal/domain"
	"context"
	"slices"
	"strings"
	"time"

	"gorm.io/gorm"
)

type DataIntegrityChecker interface {
	CheckDataIntegrity(ctx context.Context) (domain.DataIntegrityReport, error)
	ListOrphanUserLoginIDs(ctx context.Context) ([]domain.OrphanUserLoginID, error)
	DeleteOrphanUserLoginIDs(ctx context.Context, ids []string) (domain.DeleteOrphanUserLoginIDsResult, error)
}

type dataIntegrityChecker struct {
	db *gorm.DB
}

func NewDataIntegrityChecker(db *gorm.DB) DataIntegrityChecker {
	return &dataIntegrityChecker{db: db}
}

func (c *dataIntegrityChecker) CheckDataIntegrity(ctx context.Context) (domain.DataIntegrityReport, error) {
	return CheckDataIntegrity(ctx, c.db)
}

func (c *dataIntegrityChecker) ListOrphanUserLoginIDs(ctx context.Context) ([]domain.OrphanUserLoginID, error) {
	return ListOrphanUserLoginIDs(ctx, c.db, nil)
}

func (c *dataIntegrityChecker) DeleteOrphanUserLoginIDs(ctx context.Context, ids []string) (domain.DeleteOrphanUserLoginIDsResult, error) {
	return DeleteOrphanUserLoginIDs(ctx, c.db, ids)
}

func CheckDataIntegrity(ctx context.Context, db *gorm.DB) (domain.DataIntegrityReport, error) {
	tenantChecks := []domain.DataIntegrityCheck{
		{
			Key:         "duplicate_tenant_slugs",
			Label:       "중복 테넌트 slug",
			Description: "삭제되지 않은 tenant의 slug를 대소문자 무시 기준으로 검사합니다.",
			Severity:    "error",
			Count:       0,
		},
		{
			Key:         "orphan_tenant_parents",
			Label:       "유령 상위 테넌트 참조",
			Description: "tenant.parent_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.",
			Severity:    "error",
			Count:       0,
		},
	}
	userChecks := []domain.DataIntegrityCheck{
		{
			Key:         "orphan_user_tenant_memberships",
			Label:       "유령 테넌트 사용자 소속",
			Description: "users.tenant_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.",
			Severity:    "error",
			Count:       0,
		},
		{
			Key:         "orphan_user_login_id_tenants",
			Label:       "유령 테넌트 로그인 ID",
			Description: "user_login_ids.tenant_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.",
			Severity:    "error",
			Count:       0,
		},
		{
			Key:         "orphan_user_login_id_users",
			Label:       "유령 사용자 로그인 ID",
			Description: "user_login_ids.user_id가 없거나 삭제된 user를 참조하는지 검사합니다.",
			Severity:    "error",
			Count:       0,
		},
	}

	counts := []struct {
		target *int64
		query  string
	}{
		{
			target: &tenantChecks[0].Count,
			query: `
SELECT COUNT(*)
FROM (
	SELECT LOWER(TRIM(slug)) AS normalized_slug
	FROM tenants
	WHERE deleted_at IS NULL
	  AND status <> 'deleted'
	  AND TRIM(slug) <> ''
	GROUP BY LOWER(TRIM(slug))
	HAVING COUNT(*) > 1
) AS duplicate_slugs
`,
		},
		{
			target: &tenantChecks[1].Count,
			query: `
SELECT COUNT(*)
FROM tenants AS child
WHERE child.deleted_at IS NULL
  AND child.parent_id IS NOT NULL
  AND NOT EXISTS (
	SELECT 1
	FROM tenants AS parent
	WHERE parent.id = child.parent_id
	  AND parent.deleted_at IS NULL
  )
`,
		},
		{
			target: &userChecks[0].Count,
			query: `
SELECT COUNT(*)
FROM users AS u
WHERE u.deleted_at IS NULL
  AND u.tenant_id IS NOT NULL
  AND NOT EXISTS (
	SELECT 1
	FROM tenants AS t
	WHERE t.id = u.tenant_id
	  AND t.deleted_at IS NULL
  )
`,
		},
		{
			target: &userChecks[1].Count,
			query: `
SELECT COUNT(*)
FROM user_login_ids AS uli
WHERE NOT EXISTS (
	SELECT 1
	FROM tenants AS t
	WHERE t.id = uli.tenant_id
	  AND t.deleted_at IS NULL
)
`,
		},
		{
			target: &userChecks[2].Count,
			query: `
SELECT COUNT(*)
FROM user_login_ids AS uli
WHERE NOT EXISTS (
	SELECT 1
	FROM users AS u
	WHERE u.id = uli.user_id
	  AND u.deleted_at IS NULL
)
`,
		},
	}

	for _, item := range counts {
		if err := db.WithContext(ctx).Raw(item.query).Scan(item.target).Error; err != nil {
			return domain.DataIntegrityReport{}, err
		}
	}

	tenantChecks = applyIntegrityStatuses(tenantChecks)
	userChecks = applyIntegrityStatuses(userChecks)
	sections := []domain.DataIntegritySection{
		{
			Key:    "tenant_integrity",
			Label:  "테넌트 정합성",
			Status: summarizeIntegrityStatus(tenantChecks),
			Checks: tenantChecks,
		},
		{
			Key:    "user_integrity",
			Label:  "사용자 정합성",
			Status: summarizeIntegrityStatus(userChecks),
			Checks: userChecks,
		},
	}

	summary := domain.DataIntegritySummary{}
	for _, section := range sections {
		for _, check := range section.Checks {
			summary.TotalChecks++
			switch check.Status {
			case domain.DataIntegrityStatusFail:
				summary.Failures += check.Count
			case domain.DataIntegrityStatusWarning:
				summary.Warnings++
			default:
				summary.Passed++
			}
		}
	}

	return domain.DataIntegrityReport{
		Status:    summarizeSectionStatus(sections),
		CheckedAt: time.Now().UTC(),
		Summary:   summary,
		Sections:  sections,
	}, nil
}

func applyIntegrityStatuses(checks []domain.DataIntegrityCheck) []domain.DataIntegrityCheck {
	for i := range checks {
		if checks[i].Count > 0 {
			checks[i].Status = domain.DataIntegrityStatusFail
		} else {
			checks[i].Status = domain.DataIntegrityStatusPass
		}
	}
	return checks
}

func summarizeIntegrityStatus(checks []domain.DataIntegrityCheck) domain.DataIntegrityStatus {
	status := domain.DataIntegrityStatusPass
	for _, check := range checks {
		if check.Status == domain.DataIntegrityStatusFail {
			return domain.DataIntegrityStatusFail
		}
		if check.Status == domain.DataIntegrityStatusWarning {
			status = domain.DataIntegrityStatusWarning
		}
	}
	return status
}

func summarizeSectionStatus(sections []domain.DataIntegritySection) domain.DataIntegrityStatus {
	status := domain.DataIntegrityStatusPass
	for _, section := range sections {
		if section.Status == domain.DataIntegrityStatusFail {
			return domain.DataIntegrityStatusFail
		}
		if section.Status == domain.DataIntegrityStatusWarning {
			status = domain.DataIntegrityStatusWarning
		}
	}
	return status
}

func ListOrphanUserLoginIDs(ctx context.Context, db *gorm.DB, ids []string) ([]domain.OrphanUserLoginID, error) {
	type orphanRow struct {
		ID              string
		UserID          string
		UserEmail       string
		UserDeletedAt   *time.Time
		TenantID        string
		TenantSlug      string
		TenantDeletedAt *time.Time
		FieldKey        string
		LoginID         string
		MissingUser     bool
		DeletedUser     bool
		MissingTenant   bool
		DeletedTenant   bool
	}

	query := `
SELECT
	uli.id,
	uli.user_id,
	COALESCE(u.email, '') AS user_email,
	u.deleted_at AS user_deleted_at,
	uli.tenant_id,
	COALESCE(t.slug, '') AS tenant_slug,
	t.deleted_at AS tenant_deleted_at,
	uli.field_key,
	uli.login_id,
	(u.id IS NULL) AS missing_user,
	(u.id IS NOT NULL AND u.deleted_at IS NOT NULL) AS deleted_user,
	(t.id IS NULL) AS missing_tenant,
	(t.id IS NOT NULL AND t.deleted_at IS NOT NULL) AS deleted_tenant
FROM user_login_ids AS uli
LEFT JOIN users AS u ON u.id = uli.user_id
LEFT JOIN tenants AS t ON t.id = uli.tenant_id
WHERE (
	u.id IS NULL
	OR u.deleted_at IS NOT NULL
	OR t.id IS NULL
	OR t.deleted_at IS NOT NULL
)
`
	args := []any{}
	if len(ids) > 0 {
		query += " AND uli.id IN ?\n"
		args = append(args, ids)
	}
	query += "ORDER BY uli.login_id, uli.id"

	var rows []orphanRow
	if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
		return nil, err
	}

	items := make([]domain.OrphanUserLoginID, 0, len(rows))
	for _, row := range rows {
		reasons := make([]string, 0, 4)
		if row.MissingUser {
			reasons = append(reasons, "missing_user")
		}
		if row.DeletedUser {
			reasons = append(reasons, "deleted_user")
		}
		if row.MissingTenant {
			reasons = append(reasons, "missing_tenant")
		}
		if row.DeletedTenant {
			reasons = append(reasons, "deleted_tenant")
		}
		items = append(items, domain.OrphanUserLoginID{
			ID:              row.ID,
			UserID:          row.UserID,
			UserEmail:       row.UserEmail,
			UserDeletedAt:   row.UserDeletedAt,
			TenantID:        row.TenantID,
			TenantSlug:      row.TenantSlug,
			TenantDeletedAt: row.TenantDeletedAt,
			FieldKey:        row.FieldKey,
			LoginID:         row.LoginID,
			Reasons:         reasons,
		})
	}
	return items, nil
}

func DeleteOrphanUserLoginIDs(ctx context.Context, db *gorm.DB, ids []string) (domain.DeleteOrphanUserLoginIDsResult, error) {
	ids = normalizeIDList(ids)
	result := domain.DeleteOrphanUserLoginIDsResult{
		Deleted:    []domain.OrphanUserLoginID{},
		SkippedIDs: []string{},
	}
	if len(ids) == 0 {
		return result, nil
	}

	err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		items, err := ListOrphanUserLoginIDs(ctx, tx, ids)
		if err != nil {
			return err
		}

		deletableIDs := make([]string, 0, len(items))
		deletableSet := make(map[string]bool, len(items))
		for _, item := range items {
			deletableIDs = append(deletableIDs, item.ID)
			deletableSet[item.ID] = true
		}
		for _, id := range ids {
			if !deletableSet[id] {
				result.SkippedIDs = append(result.SkippedIDs, id)
			}
		}
		if len(deletableIDs) == 0 {
			return nil
		}

		deleteResult := tx.Exec("DELETE FROM user_login_ids WHERE id IN ?", deletableIDs)
		if deleteResult.Error != nil {
			return deleteResult.Error
		}
		result.Deleted = items
		result.DeletedCount = deleteResult.RowsAffected
		return nil
	})
	return result, err
}

func normalizeIDList(ids []string) []string {
	normalized := make([]string, 0, len(ids))
	seen := map[string]bool{}
	for _, id := range ids {
		id = strings.TrimSpace(id)
		if id == "" || seen[id] {
			continue
		}
		seen[id] = true
		normalized = append(normalized, id)
	}
	slices.Sort(normalized)
	return normalized
}
