package domain

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	VerificationDomainNamePrefix = "_tidbcloud-challenge"
	verificationTokenPrefix      = "tidbcloud-verify="
	ownerConfirmedPeriod         = 60 * 24 * time.Hour
)

var domainNameRegex = regexp.MustCompile(`^([a-z0-9_]([a-z0-9_-]*[a-z0-9_])?\.)+[a-z]{2,}$`)

type OrgDomainSvcIface interface {
	AddDomain(ctx context.Context, orgID uint64, domainName string) (*entity.OrgDomain, error)
	VerifyDomain(ctx context.Context, orgID uint64, domainID uint64) (*entity.OrgDomain, error)
	ConfirmDomains(ctx context.Context, orgID uint64, domainIDs []uint64, confirmedBy string) error
	DeleteDomain(ctx context.Context, orgID uint64, domainID uint64) error
	ListDomains(ctx context.Context, orgID uint64) ([]entity.OrgDomain, error)
	IsDomainVerified(ctx context.Context, orgID uint64, domainName string) (bool, error)
}

var (
	orgDomainSvcInitOnce sync.Once
	orgDomainSvcInstance OrgDomainSvcIface
)

func GetOrgDomainSvc() (OrgDomainSvcIface, error) {
	orgDomainSvcInitOnce.Do(func() {
		orgDomainSvcInstance = &OrgDomainSvc{}
	})
	return orgDomainSvcInstance, nil
}

type OrgDomainSvc struct{}

func getDB() *gorm.DB {
	return database.DBConn.DB
}

func getHub() *dao.DaoHub {
	return dao.GetDaoHub()
}

func (s *OrgDomainSvc) AddDomain(ctx context.Context, orgID uint64, domainName string) (*entity.OrgDomain, error) {
	domainName = strings.ToLower(strings.TrimSpace(domainName))
	if !domainNameRegex.MatchString(domainName) {
		return nil, errs.NewBadRequest("invalid domain name format: "+domainName, nil)
	}

	existing, err := getHub().OrgDomain.GetByOrgAndDomain(ctx, getDB(), orgID, domainName)
	if err != nil {
		return nil, errs.NewInternalDB("failed to get domain by org and domain", err)
	}
	if existing != nil {
		return nil, errs.NewBadRequest("domain already exists for this organization: "+domainName, nil)
	}

	token := verificationTokenPrefix + uuid.New().String()
	d := &entity.OrgDomain{
		OrgID:             entity.ID(orgID),
		DomainName:        domainName,
		Status:            entity.DomainStatusUnverified,
		VerificationToken: token,
	}

	created, err := getHub().OrgDomain.Create(ctx, getDB(), d)
	if err != nil {
		tilog.Error(ctx, "AddDomain create failed", zap.Uint64("orgID", orgID), zap.String("domain", domainName), zap.Error(err))
		return nil, errs.NewInternalDB("failed to create domain", err)
	}

	tilog.Info(ctx, "AddDomain success", zap.Uint64("orgID", orgID), zap.String("domain", domainName), zap.String("token", token))
	return created, nil
}

func (s *OrgDomainSvc) VerifyDomain(ctx context.Context, orgID uint64, domainID uint64) (*entity.OrgDomain, error) {
	d, err := getHub().OrgDomain.GetByID(ctx, getDB(), domainID)
	if err != nil {
		return nil, errs.NewInternalDB("failed to get domain by id", err)
	}
	if d == nil {
		return nil, errs.NewBadRequest("domain not found", nil)
	}
	if d.OrgID.Uint64() != orgID {
		return nil, errs.NewBadRequest("domain does not belong to this organization", nil)
	}
	if d.Status == entity.DomainStatusVerified {
		return d, nil
	}

	verificationDomainName := fmt.Sprintf("%s.%s", VerificationDomainNamePrefix, d.DomainName)
	matched, err := NewDnsTxtVerifier().VerifyTXTRecord(ctx, verificationDomainName, d.VerificationToken)
	if err != nil {
		tilog.Error(ctx, "VerifyDomain DNS check failed", zap.String("domain", d.DomainName), zap.Error(err))
		return nil, errs.NewBadRequest("DNS verification failed: "+err.Error(), nil)
	}
	if !matched {
		return nil, errs.NewBadRequest("DNS TXT record not found or does not match. Please add the following TXT record to your DNS: "+d.VerificationToken, nil)
	}

	now := time.Now().UTC()
	d.Status = entity.DomainStatusVerified
	d.VerifiedAt = &now

	err = getHub().OrgDomain.Update(ctx, getDB(), d)
	if err != nil {
		tilog.Error(ctx, "VerifyDomain update failed", zap.Uint64("domainID", domainID), zap.Error(err))
		return nil, errs.NewInternalDB("failed to update domain status", err)
	}

	tilog.Info(ctx, "VerifyDomain success", zap.Uint64("orgID", orgID), zap.String("domain", d.DomainName))
	return d, nil
}

func (s *OrgDomainSvc) ConfirmDomains(ctx context.Context, orgID uint64, domainIDs []uint64, confirmedBy string) error {
	if len(domainIDs) == 0 {
		return nil
	}

	domains, err := getHub().OrgDomain.ListByIDsAndOrgID(ctx, getDB(), domainIDs, orgID)
	if err != nil {
		return errs.NewInternalDB("failed to list domains by ids and org id", err)
	}
	if len(domains) != len(domainIDs) {
		return errs.NewBadRequest("some domain ids not found or do not belong to this organization", nil)
	}

	var needConfirmIDs []uint64
	for _, d := range domains {
		if d.Status != entity.DomainStatusVerified && d.Status != entity.DomainStatusOwnerConfirmed {
			needConfirmIDs = append(needConfirmIDs, d.ID.Uint64())
		}
	}
	if len(needConfirmIDs) == 0 {
		return nil
	}

	now := time.Now().UTC()
	expiresAt := now.Add(ownerConfirmedPeriod)
	fields := map[string]interface{}{
		"status":       entity.DomainStatusOwnerConfirmed,
		"confirmed_at": now,
		"confirmed_by": confirmedBy,
		"expires_at":   expiresAt,
	}

	err = getHub().OrgDomain.BatchUpdateByIDs(ctx, getDB(), needConfirmIDs, fields)
	if err != nil {
		return errs.NewInternalDB("failed to batch update domain status", err)
	}
	tilog.Info(ctx, "ConfirmDomains success", zap.Uint64("orgID", orgID), zap.Uint64s("domainIDs", needConfirmIDs), zap.String("confirmedBy", confirmedBy))
	return nil
}

func (s *OrgDomainSvc) DeleteDomain(ctx context.Context, orgID uint64, domainID uint64) error {
	d, err := getHub().OrgDomain.GetByID(ctx, getDB(), domainID)
	if err != nil {
		return errs.NewInternalDB("failed to get domain by id", err)
	}
	if d == nil {
		return errs.NewBadRequest("domain not found", nil)
	}
	if d.OrgID.Uint64() != orgID {
		return errs.NewBadRequest("domain does not belong to this organization", nil)
	}

	err = getHub().OrgDomain.Delete(ctx, getDB(), domainID)
	if err != nil {
		tilog.Error(ctx, "DeleteDomain failed", zap.Uint64("domainID", domainID), zap.Error(err))
		return errs.NewInternalDB("failed to delete domain", err)
	}

	tilog.Info(ctx, "DeleteDomain success", zap.Uint64("orgID", orgID), zap.Uint64("domainID", domainID))
	return nil
}

func (s *OrgDomainSvc) ListDomains(ctx context.Context, orgID uint64) ([]entity.OrgDomain, error) {
	domains, err := getHub().OrgDomain.ListByOrgID(ctx, getDB(), orgID)
	if err != nil {
		return nil, errs.NewInternalDB("failed to list domains by org id", err)
	}
	return domains, nil
}

func (s *OrgDomainSvc) IsDomainVerified(ctx context.Context, orgID uint64, domainName string) (bool, error) {
	hub := dao.GetDaoHub()
	db := database.DBConn.DB
	d, err := hub.OrgDomain.GetByOrgAndDomain(ctx, db, orgID, domainName)
	if err != nil {
		return false, errs.NewInternalDB("failed to get domain by org and domain", err)
	}
	if d == nil {
		return false, nil
	}
	return d.Status == entity.DomainStatusVerified, nil
}
