package repository

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"

	caasmmodel "security-center-backend/internal/modules/caasm/model"
)

type DomainFilter struct {
	Query         string
	Environment   string
	Provider      caasmmodel.CloudProvider
	Status        string // active, expired, expiring
	ZoneType      string
	DomainClass   string
	Registrar     string
	HasForwarding *bool
	HasPeering    *bool
	AutoRenew     *bool
	SortBy        string
	SortOrder     string
}

type DomainStats struct {
	Total        int64 `json:"total"`
	ExpiringSoon int64 `json:"expiring_soon"`
	Expired      int64 `json:"expired"`
}

type DomainRepository interface {
	List(ctx context.Context, filter DomainFilter, page, pageSize int) ([]caasmmodel.DomainAsset, int64, error)
	Stats(ctx context.Context, window time.Duration) (*DomainStats, error)
	FindByID(ctx context.Context, id uint64) (*caasmmodel.DomainAsset, error)
	GetRegistrars(ctx context.Context) ([]string, error)
}

type GormDomainRepository struct {
	db *gorm.DB
}

func NewDomainRepository(db *gorm.DB) DomainRepository {
	return &GormDomainRepository{db: db}
}

func canonicalDomainClass(value string) string {
	switch strings.TrimSpace(strings.ToLower(value)) {
	case "authoritative_zone", "hosted_zone":
		return caasmmodel.DomainClassAuthoritativeZone
	case "dns_record", "record_derived_name":
		return caasmmodel.DomainClassDNSRecord
	default:
		return strings.TrimSpace(strings.ToLower(value))
	}
}

func jsonObjectHasEntriesCondition(dialect, column string, hasEntries bool) string {
	if dialect == "sqlite" {
		hasCondition := fmt.Sprintf("(%[1]s IS NOT NULL AND json_type(%[1]s) = 'object' AND EXISTS (SELECT 1 FROM json_each(%[1]s)))", column)
		if hasEntries {
			return hasCondition
		}
		return fmt.Sprintf("(%[1]s IS NULL OR json_type(%[1]s) IS NULL OR json_type(%[1]s) <> 'object' OR NOT EXISTS (SELECT 1 FROM json_each(%[1]s)))", column)
	}
	hasCondition := fmt.Sprintf("(%[1]s IS NOT NULL AND JSON_TYPE(%[1]s) = 'OBJECT' AND JSON_LENGTH(%[1]s) > 0)", column)
	if hasEntries {
		return hasCondition
	}
	return fmt.Sprintf("(%[1]s IS NULL OR JSON_TYPE(%[1]s) <> 'OBJECT' OR JSON_LENGTH(%[1]s) = 0)", column)
}

func (r *GormDomainRepository) List(ctx context.Context, filter DomainFilter, page, pageSize int) ([]caasmmodel.DomainAsset, int64, error) {
	var (
		domains []caasmmodel.DomainAsset
		total   int64
	)
	inactiveStatus := string(caasmmodel.AssetStatusInactive)

	query := r.db.WithContext(ctx).Model(&caasmmodel.DomainAsset{}).
		Preload("Asset").
		Preload("Asset.CloudAccount").
		Preload("Asset.OwnerEmployee").
		Preload("Asset.Department").
		Preload("OwnerEmployee").
		Preload("Department").
		Joins("LEFT JOIN assets ON assets.id = domain_assets.asset_id").
		Joins("LEFT JOIN cloud_accounts ON cloud_accounts.id = assets.cloud_account_id").
		Where("domain_assets.deleted_at IS NULL").
		Where("(assets.id IS NULL OR assets.deleted_at IS NULL)").
		Where("(cloud_accounts.id IS NULL OR cloud_accounts.deleted_at IS NULL)")

	if filter.Status != "inactive" {
		query = query.
			Where("(domain_assets.status IS NULL OR domain_assets.status <> ?)", inactiveStatus).
			Where("(assets.id IS NULL OR assets.status <> ?)", caasmmodel.AssetStatusInactive)
	}

	if filter.Query != "" {
		like := "%" + filter.Query + "%"
		query = query.Where("domain_assets.domain_name LIKE ?", like)
	}
	if filter.Environment != "" {
		query = query.Where("assets.environment = ?", filter.Environment)
	}
	if filter.Provider != "" {
		query = query.Where("cloud_accounts.provider = ?", filter.Provider)
	}
	if filter.ZoneType != "" {
		query = query.Where("domain_assets.zone_type = ?", filter.ZoneType)
	}
	if filter.DomainClass != "" {
		switch canonicalDomainClass(filter.DomainClass) {
		case caasmmodel.DomainClassAuthoritativeZone:
			query = query.Where("domain_assets.domain_class IN ?", []string{
				caasmmodel.DomainClassAuthoritativeZone,
				"authoritative_zone",
			})
		case caasmmodel.DomainClassDNSRecord:
			query = query.Where("domain_assets.domain_class IN ?", []string{
				caasmmodel.DomainClassDNSRecord,
				"dns_record",
			})
		default:
			query = query.Where("domain_assets.domain_class = ?", filter.DomainClass)
		}
	}
	if filter.Registrar != "" {
		switch canonicalDomainRegistrar(filter.Registrar) {
		case "route53":
			query = query.Where(
				"(LOWER(REPLACE(domain_assets.registrar, ' ', '')) LIKE ? OR LOWER(domain_assets.registrar) = ?)",
				"%route53%",
				"amazon route 53",
			)
		default:
			like := "%" + filter.Registrar + "%"
			query = query.Where("domain_assets.registrar LIKE ?", like)
		}
	}
	if filter.HasForwarding != nil {
		query = query.Where(jsonObjectHasEntriesCondition(r.db.Dialector.Name(), "domain_assets.forwarding_cfg", *filter.HasForwarding))
	}
	if filter.HasPeering != nil {
		query = query.Where(jsonObjectHasEntriesCondition(r.db.Dialector.Name(), "domain_assets.peering_cfg", *filter.HasPeering))
	}
	if filter.AutoRenew != nil {
		if *filter.AutoRenew {
			query = query.Where("domain_assets.auto_renew = 1")
		} else {
			query = query.Where("domain_assets.auto_renew = 0")
		}
	}
	now := time.Now()
	switch filter.Status {
	case "inactive":
		query = query.Where("(domain_assets.status = ? OR assets.status = ?)", inactiveStatus, caasmmodel.AssetStatusInactive)
	case "expired":
		query = query.Where("domain_assets.expires_at IS NOT NULL AND domain_assets.expires_at < ?", now)
	case "expiring":
		query = query.Where("domain_assets.expires_at BETWEEN ? AND ?", now, now.Add(30*24*time.Hour))
	case "active":
		query = query.Where("domain_assets.status = ?", "active").
			Where("domain_assets.expires_at IS NULL OR domain_assets.expires_at >= ?", now)
	}

	if err := query.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	orderClause := "domain_assets.expires_at IS NULL DESC, domain_assets.expires_at ASC"
	switch filter.SortBy {
	case "last_synced_at":
		if strings.EqualFold(filter.SortOrder, "asc") {
			orderClause = "domain_assets.last_synced_at ASC"
		} else {
			orderClause = "domain_assets.last_synced_at DESC"
		}
	case "name":
		if strings.EqualFold(filter.SortOrder, "desc") {
			orderClause = "domain_assets.domain_name DESC"
		} else {
			orderClause = "domain_assets.domain_name ASC"
		}
	case "expires_at":
		if strings.EqualFold(filter.SortOrder, "desc") {
			orderClause = "domain_assets.expires_at DESC"
		} else {
			orderClause = "domain_assets.expires_at ASC"
		}
	}

	if err := query.Order(orderClause).
		Limit(pageSize).
		Offset((page - 1) * pageSize).
		Find(&domains).Error; err != nil {
		return nil, 0, err
	}
	return domains, total, nil
}

func (r *GormDomainRepository) Stats(ctx context.Context, window time.Duration) (*DomainStats, error) {
	var stats DomainStats
	base := r.db.WithContext(ctx).
		Model(&caasmmodel.DomainAsset{}).
		Joins("LEFT JOIN assets ON assets.id = domain_assets.asset_id").
		Where("domain_assets.deleted_at IS NULL").
		Where("(assets.id IS NULL OR assets.deleted_at IS NULL)").
		Where("(domain_assets.status IS NULL OR domain_assets.status <> ?)", string(caasmmodel.AssetStatusInactive)).
		Where("(assets.id IS NULL OR assets.status <> ?)", caasmmodel.AssetStatusInactive)
	if err := base.Count(&stats.Total).Error; err != nil {
		return nil, err
	}

	now := time.Now()
	expiringBefore := now.Add(window)
	if err := base.Session(&gorm.Session{}).
		Where("expires_at IS NOT NULL AND expires_at BETWEEN ? AND ?", now, expiringBefore).
		Count(&stats.ExpiringSoon).Error; err != nil {
		return nil, err
	}

	if err := base.Session(&gorm.Session{}).
		Where("expires_at IS NOT NULL AND expires_at < ?", now).
		Count(&stats.Expired).Error; err != nil {
		return nil, err
	}
	return &stats, nil
}

func (r *GormDomainRepository) FindByID(ctx context.Context, id uint64) (*caasmmodel.DomainAsset, error) {
	var domain caasmmodel.DomainAsset
	if err := r.db.WithContext(ctx).
		Joins("LEFT JOIN assets ON assets.id = domain_assets.asset_id").
		Joins("LEFT JOIN cloud_accounts ON cloud_accounts.id = assets.cloud_account_id").
		Preload("Asset").
		Preload("Asset.CloudAccount").
		Preload("Asset.OwnerEmployee").
		Preload("Asset.Department").
		Preload("OwnerEmployee").
		Preload("Department").
		Where("domain_assets.deleted_at IS NULL").
		Where("(assets.id IS NULL OR assets.deleted_at IS NULL)").
		Where("(domain_assets.status IS NULL OR domain_assets.status <> ?)", string(caasmmodel.AssetStatusInactive)).
		Where("(assets.id IS NULL OR assets.status <> ?)", caasmmodel.AssetStatusInactive).
		Where("(cloud_accounts.id IS NULL OR cloud_accounts.deleted_at IS NULL)").
		First(&domain, id).Error; err != nil {
		return nil, err
	}
	return &domain, nil
}

func (r *GormDomainRepository) GetRegistrars(ctx context.Context) ([]string, error) {
	var rawRegistrars []string
	if err := r.db.WithContext(ctx).
		Model(&caasmmodel.DomainAsset{}).
		Joins("LEFT JOIN assets ON assets.id = domain_assets.asset_id").
		Where("domain_assets.deleted_at IS NULL").
		Where("(assets.id IS NULL OR assets.deleted_at IS NULL)").
		Where("(domain_assets.status IS NULL OR domain_assets.status <> ?)", string(caasmmodel.AssetStatusInactive)).
		Where("(assets.id IS NULL OR assets.status <> ?)", caasmmodel.AssetStatusInactive).
		Distinct("registrar").
		Where("registrar != ''").
		Order("registrar ASC").
		Pluck("registrar", &rawRegistrars).Error; err != nil {
		return nil, err
	}

	registrarSet := make(map[string]struct{}, len(rawRegistrars))
	for _, raw := range rawRegistrars {
		canonical := canonicalDomainRegistrar(raw)
		if canonical == "" {
			continue
		}
		registrarSet[canonical] = struct{}{}
	}
	registrars := make([]string, 0, len(registrarSet))
	for registrar := range registrarSet {
		registrars = append(registrars, registrar)
	}
	sort.Strings(registrars)
	return registrars, nil
}

func canonicalDomainRegistrar(value string) string {
	trimmed := strings.TrimSpace(strings.ToLower(value))
	if trimmed == "" {
		return ""
	}
	switch strings.ReplaceAll(trimmed, " ", "") {
	case "amazonroute53", "route53":
		return "route53"
	default:
		return trimmed
	}
}
