package service

import (
	"bytes"
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/go-errors/errors"

	"github.com/caddyserver/certmagic"
	"github.com/google/uuid"
	"github.com/oapi-codegen/nullable"
	"github.com/phishingclub/phishingclub/acme"
	"github.com/phishingclub/phishingclub/build"
	"github.com/phishingclub/phishingclub/data"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/model"
	"github.com/phishingclub/phishingclub/repository"
	"github.com/phishingclub/phishingclub/validate"
	"github.com/phishingclub/phishingclub/vo"
	"gorm.io/gorm"
)

// Domain is a Domain service
type Domain struct {
	Common
	OwnManagedCertificatePath string
	CertMagicConfig           *certmagic.Config
	CertMagicCache            *certmagic.Cache
	DomainRepository          *repository.Domain
	CompanyRepository         *repository.Company
	OptionRepository          *repository.Option
	CampaignTemplateService   *CampaignTemplate
	AssetService              *Asset
	FileService               *File
	TemplateService           *Template
}

// CreateProxyDomain creates a proxy domain bypassing direct creation restrictions
func (d *Domain) CreateProxyDomain(
	ctx context.Context,
	session *model.Session,
	domain *model.Domain,
) (*uuid.UUID, error) {
	return d.createDomain(ctx, session, domain, true)
}

// Create creates a new domain
func (d *Domain) Create(
	ctx context.Context,
	session *model.Session,
	domain *model.Domain,
) (*uuid.UUID, error) {
	return d.createDomain(ctx, session, domain, false)
}

// DeleteProxyDomain deletes a proxy domain bypassing direct deletion restrictions
func (d *Domain) DeleteProxyDomain(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
) error {
	return d.deleteDomain(ctx, session, id, true)
}

// UpdateProxyDomain updates a proxy domain bypassing direct update restrictions
func (d *Domain) UpdateProxyDomain(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	incoming *model.Domain,
) error {
	return d.updateDomain(ctx, session, id, incoming, true)
}

// createDomain is the internal domain creation method
func (d *Domain) createDomain(
	ctx context.Context,
	session *model.Session,
	domain *model.Domain,
	allowProxyCreation bool,
) (*uuid.UUID, error) {
	ae := NewAuditEvent("Domain.Create", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return nil, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	// prevent direct creation of proxy domains unless explicitly allowed
	if !allowProxyCreation {
		if domainType, err := domain.Type.Get(); err == nil && domainType.String() == "proxy" {
			return nil, validate.WrapErrorWithField(errors.New("proxy domains can only be created through proxy configuration, not directly"), "type")
		}
	}

	// validate data
	if err := domain.Validate(); err != nil {
		d.Logger.Errorw("failed to validate domain", "error", err)
		return nil, errs.Wrap(err)
	}

	// get domain type for specific validation
	domainType, _ := domain.Type.Get()

	if domainType.String() == "proxy" {
		// validate proxy target domain
		if err := d.validateProxyDomain(ctx, domain); err != nil {
			return nil, err
		}
	} else {
		// validate template content for regular domains
		if pageContent, err := domain.PageContent.Get(); err == nil {
			if err := d.TemplateService.ValidateDomainTemplate(pageContent.String()); err != nil {
				d.Logger.Errorw("failed to validate domain page template", "error", err)
				return nil, validate.WrapErrorWithField(errors.New("invalid page template: "+err.Error()), "pageContent")
			}
		}
		if notFoundContent, err := domain.PageNotFoundContent.Get(); err == nil {
			if err := d.TemplateService.ValidateDomainTemplate(notFoundContent.String()); err != nil {
				d.Logger.Errorw("failed to validate domain not found template", "error", err)
				return nil, validate.WrapErrorWithField(errors.New("invalid not found template: "+err.Error()), "pageNotFoundContent")
			}
		}
	}
	// check for uniqueness
	name := domain.Name.MustGet() // safe as we have validated
	_, err = d.DomainRepository.GetByName(
		ctx,
		&name,
		&repository.DomainOption{},
	)
	// we expect not to find a domain with this name
	if err != nil {
		// something went wrong
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			d.Logger.Errorw("failed to create domain", "error", err)
			return nil, errs.Wrap(err)
		}
	}
	// if there is no error, it means we found a domain with this name
	if err == nil {
		d.Logger.Debugw("domain name is already taken", "error", name.String())
		return nil, validate.WrapErrorWithField(errors.New("not unique"), "name")
	}
	domain, err = d.handleOwnManagedTLS(ctx, domain)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	domain, err = d.handleSelfSignedTLS(ctx, domain)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	// create domain
	createdDomainID, err := d.DomainRepository.Insert(
		ctx,
		domain,
	)
	if err != nil {
		d.Logger.Errorw("failed to create domain", "error", err)
		return nil, errs.Wrap(err)
	}
	ae.Details["id"] = createdDomainID.String()
	d.AuditLogAuthorized(ae)
	if domain.ManagedTLS.MustGet() && build.Flags.Production {
		d.Logger.Debugw("triggering certificate retrieval", "domain", name.String())
		d.triggerCertificateRetrieval(name.String())
	}
	return createdDomainID, nil
}

// triggerCertificateRetrieval attempts to trigger automatic certificate
// by making an HTTPS request to the domain
func (d *Domain) triggerCertificateRetrieval(name string) {
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		transport := &http.Transport{
			TLSClientConfig: &tls.Config{
				// #nosec
				InsecureSkipVerify: true, // since cert won't be valid yet
			},
			// Set reasonable timeouts
			DialContext: (&net.Dialer{
				Timeout:   10 * time.Second,
				KeepAlive: 5 * time.Second,
			}).DialContext,
			TLSHandshakeTimeout:   10 * time.Second,
			ResponseHeaderTimeout: 10 * time.Second,
			ExpectContinueTimeout: 1 * time.Second,
			IdleConnTimeout:       5 * time.Second,
			// disable connection pooling since we only need one request
			DisableKeepAlives: true,
			MaxIdleConns:      -1,
		}

		client := &http.Client{
			Transport: transport,
			// don't need client timeout as we're using context
		}

		req, err := http.NewRequestWithContext(ctx, "GET", "https://"+name, nil)
		if err != nil {
			d.Logger.Errorw("failed to create request",
				"domain", name,
				"error", err)
			return
		}

		resp, err := client.Do(req)
		if err != nil {
			d.Logger.Errorw("failed to trigger certificate retrieval",
				"domain", name,
				"error", err)
			return
		}
		// always close response body
		if resp != nil && resp.Body != nil {
			defer resp.Body.Close()
		}

		// clean up transport
		transport.CloseIdleConnections()

		d.Logger.Debugw("certificate retrieval triggered",
			"domain", name,
			"status", resp.StatusCode)
	}()
}

// GetAll gets domains
func (d *Domain) GetAll(
	companyID *uuid.UUID, // can be null
	ctx context.Context,
	session *model.Session,
	queryArgs *vo.QueryArgs,
	withCompany bool,
) (*model.Result[model.Domain], error) {
	result := model.NewEmptyResult[model.Domain]()
	ae := NewAuditEvent("Domain.GetAll", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return result, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return result, errs.ErrAuthorizationFailed
	}
	// get domains
	result, err = d.DomainRepository.GetAll(
		ctx,
		companyID,
		&repository.DomainOption{
			QueryArgs:   queryArgs,
			WithCompany: withCompany,
		},
	)
	if err != nil {
		d.Logger.Errorw("failed to get domains", "error", err)
		return result, errs.Wrap(err)
	}
	// no audit on read
	return result, nil
}

// GetByName gets a domain by name
func (d *Domain) GetByName(
	ctx context.Context,
	session *model.Session,
	name *vo.String255,
	options *repository.DomainOption,
) (*model.Domain, error) {
	ae := NewAuditEvent("Domain.GetByName", session)
	ae.Details["name"] = name.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return nil, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	// get domain
	domain, err := d.DomainRepository.GetByName(
		ctx,
		name,
		options,
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		// return early, this is not an error
		return nil, errs.Wrap(err)
	}
	if err != nil {
		d.Logger.Errorw("failed to get domain by name", "error", err)
		return nil, errs.Wrap(err)
	}
	// no audit on read
	return domain, nil
}

// GetAllOverview gets domains with limited data
func (d *Domain) GetAllOverview(
	companyID *uuid.UUID, // can be null
	ctx context.Context,
	session *model.Session,
	queryArgs *vo.QueryArgs,
) (*model.Result[model.DomainOverview], error) {
	result := model.NewEmptyResult[model.DomainOverview]()
	ae := NewAuditEvent("Domain.GetAllOverview", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return result, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return result, errs.ErrAuthorizationFailed
	}
	// get domains
	result, err = d.DomainRepository.GetAllSubset(
		ctx,
		companyID,
		&repository.DomainOption{
			QueryArgs: queryArgs,
		},
	)
	if err != nil {
		d.Logger.Errorw("failed to get domains subset", "error", err)
		return result, errs.Wrap(err)
	}
	// no audit on read
	return result, nil
}

// GetAllOverviewWithoutProxies gets domains with limited data, excluding proxy domains for asset management
func (d *Domain) GetAllOverviewWithoutProxies(
	companyID *uuid.UUID, // can be null
	ctx context.Context,
	session *model.Session,
	queryArgs *vo.QueryArgs,
) (*model.Result[model.DomainOverview], error) {
	result := model.NewEmptyResult[model.DomainOverview]()
	ae := NewAuditEvent("Domain.GetAllOverviewWithoutProxies", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return result, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return result, errs.ErrAuthorizationFailed
	}
	// get domains excluding proxy domains for asset management
	result, err = d.DomainRepository.GetAllSubset(
		ctx,
		companyID,
		&repository.DomainOption{
			QueryArgs:           queryArgs,
			ExcludeProxyDomains: true,
		},
	)
	if err != nil {
		d.Logger.Errorw("failed to get domains subset for assets", "error", err)
		return result, errs.Wrap(err)
	}
	// no audit on read
	return result, nil
}

// GetByID is a function to get domain by id
func (d *Domain) GetByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	options *repository.DomainOption,
) (*model.Domain, error) {
	ae := NewAuditEvent("Domain.GetByID", session)
	ae.Details["id"] = id.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return nil, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	// get domain
	domain, err := d.DomainRepository.GetByID(
		ctx,
		id,
		options,
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		// return early, this is not an error
		return nil, errs.Wrap(err)
	}
	if err != nil {
		d.Logger.Errorw("failed to get domain by id", "error", err)
		return nil, errs.Wrap(err)
	}
	return domain, nil
}

// GetByCompanyID is a function to get domain by company id
func (d *Domain) GetByCompanyID(
	ctx context.Context,
	session *model.Session,
	companyID *uuid.UUID,
	options *repository.DomainOption,
) (*model.Result[model.Domain], error) {
	result := model.NewEmptyResult[model.Domain]()
	ae := NewAuditEvent("Domain.GetByCompanyID", session)
	ae.Details["companyId"] = companyID.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return result, errs.Wrap(err)
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return result, errs.ErrAuthorizationFailed
	}
	// get domains
	result, err = d.DomainRepository.GetAllByCompanyID(
		ctx,
		companyID,
		options,
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		// return early, this is not an error
		return result, errs.Wrap(err)
	}
	if err != nil {
		d.Logger.Errorw("failed to get domain by company id", "error", err)
		return result, errs.Wrap(err)
	}
	// no audit on read
	return result, nil
}

// UpdateByID updates domain by id
func (d *Domain) UpdateByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	incoming *model.Domain,
) error {
	return d.updateDomain(ctx, session, id, incoming, false)
}

// updateDomain is the internal domain update method
func (d *Domain) updateDomain(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	incoming *model.Domain,
	allowProxyUpdate bool,
) error {
	ae := NewAuditEvent("Domain.UpdateByID", session)
	ae.Details["id"] = id.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return err
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return errs.ErrAuthorizationFailed
	}
	// get the domain that is being updated
	current, err := d.DomainRepository.GetByID(
		ctx,
		id,
		&repository.DomainOption{},
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		d.Logger.Debugw("domain not found", "error", err)
		return err
	}
	if err != nil {
		d.Logger.Errorw("failed to update domain", "error", err)
		return err
	}

	// check if this is a proxy domain and restrict editable fields
	isProxyDomain := false
	if domainType, err := current.Type.Get(); err == nil && domainType.String() == "proxy" {
		isProxyDomain = true
		// for proxy domains, only allow updating ManagedTLS and custom certificate fields
		if incoming.Type.IsSpecified() {
			incomingType, _ := incoming.Type.Get()
			if incomingType.String() != "proxy" {
				return validate.WrapErrorWithField(errors.New("cannot change type of proxy domain"), "type")
			}
		}
	} else {
		// prevent changing regular domains to proxy type
		if incoming.Type.IsSpecified() {
			incomingType, _ := incoming.Type.Get()
			if incomingType.String() == "proxy" {
				return validate.WrapErrorWithField(errors.New("cannot change domain to proxy type - proxy domains can only be created through proxy configuration"), "type")
			}
		}
	}

	// set the supplied field on the existing domain
	if isProxyDomain && !allowProxyUpdate {
		// for proxy domains, prevent changing proxy-specific fields unless explicitly allowed
		if incoming.ProxyTargetDomain.IsSpecified() {
			return validate.WrapErrorWithField(errors.New("cannot change proxy target domain - edit the proxy configuration instead"), "proxyTargetDomain")
		}
		if incoming.HostWebsite.IsSpecified() {
			return validate.WrapErrorWithField(errors.New("cannot change host website setting for proxy domain"), "hostWebsite")
		}
		if incoming.PageContent.IsSpecified() {
			return validate.WrapErrorWithField(errors.New("cannot change page content for proxy domain"), "pageContent")
		}
		if incoming.PageNotFoundContent.IsSpecified() {
			return validate.WrapErrorWithField(errors.New("cannot change page not found content for proxy domain"), "pageNotFoundContent")
		}
		if incoming.RedirectURL.IsSpecified() {
			return validate.WrapErrorWithField(errors.New("cannot change redirect URL for proxy domain"), "redirectURL")
		}
	} else {
		// for regular domains or proxy domains with allowed updates, allow updating all fields
		if v, err := incoming.Type.Get(); err == nil {
			current.Type.Set(v)
		}
		if v, err := incoming.ProxyTargetDomain.Get(); err == nil {
			current.ProxyTargetDomain.Set(v)
		}
		if v, err := incoming.HostWebsite.Get(); err == nil {
			current.HostWebsite.Set(v)
		}
		if v, err := incoming.PageContent.Get(); err == nil {
			// validate template content before updating
			if err := d.TemplateService.ValidateDomainTemplate(v.String()); err != nil {
				d.Logger.Errorw("failed to validate domain page template", "error", err)
				return validate.WrapErrorWithField(errors.New("invalid page template: "+err.Error()), "pageContent")
			}
			current.PageContent.Set(v)
		}
		if v, err := incoming.PageNotFoundContent.Get(); err == nil {
			// validate template content before updating
			if err := d.TemplateService.ValidateDomainTemplate(v.String()); err != nil {
				d.Logger.Errorw("failed to validate domain not found template", "error", err)
				return validate.WrapErrorWithField(errors.New("invalid not found template: "+err.Error()), "pageNotFoundContent")
			}
			current.PageNotFoundContent.Set(v)
		}
		if v, err := incoming.RedirectURL.Get(); err == nil {
			current.RedirectURL.Set(v)
		}
	}

	wasManagedTLS := current.ManagedTLS.MustGet()
	if v, err := incoming.ManagedTLS.Get(); err == nil {
		current.ManagedTLS.Set(v)
	}
	wasOwnManagedTLS := current.OwnManagedTLS.MustGet()
	ownManagedTLSIsSet := false
	if v, err := incoming.OwnManagedTLS.Get(); err == nil {
		current.OwnManagedTLS.Set(v)
		ownManagedTLSIsSet = v
	}
	wasSelfSignedTLS := current.SelfSignedTLS.MustGet()
	selfSignedTLSIsSet := false
	if v, err := incoming.SelfSignedTLS.Get(); err == nil {
		current.SelfSignedTLS.Set(v)
		selfSignedTLSIsSet = v
	}
	ownManagedTLSKeyIsSet := false
	if v, err := incoming.OwnManagedTLSKey.Get(); err == nil {
		current.OwnManagedTLSKey.Set(v)
		ownManagedTLSKeyIsSet = len(v) > 0
	}
	ownManagedTLSPemIsSet := false
	if v, err := incoming.OwnManagedTLSPem.Get(); err == nil {
		current.OwnManagedTLSPem.Set(v)
		ownManagedTLSPemIsSet = len(v) > 0
	}

	// validate
	if err := current.Validate(); err != nil {
		d.Logger.Errorw("failed to validate domain", "error", err)
		return err
	}

	// validate proxy domain if type is proxy
	if domainType, err := current.Type.Get(); err == nil && domainType.String() == "proxy" {
		if err := d.validateProxyDomain(ctx, current); err != nil {
			return err
		}
	} else {
		// validate template content for regular domains only
		if pageContent, err := current.PageContent.Get(); err == nil {
			if err := d.TemplateService.ValidateDomainTemplate(pageContent.String()); err != nil {
				d.Logger.Errorw("failed to validate domain page template", "error", err)
				return validate.WrapErrorWithField(errors.New("invalid page template: "+err.Error()), "pageContent")
			}
		}
		if notFoundContent, err := current.PageNotFoundContent.Get(); err == nil {
			if err := d.TemplateService.ValidateDomainTemplate(notFoundContent.String()); err != nil {
				d.Logger.Errorw("failed to validate domain not found template", "error", err)
				return validate.WrapErrorWithField(errors.New("invalid not found template: "+err.Error()), "pageNotFoundContent")
			}
		}
	}
	// clean up if TLS was previous managed but no longer is
	if managedTLS, err := incoming.ManagedTLS.Get(); err == nil && !managedTLS {
		if wasManagedTLS {
			d.removeManagedDomainTLS(ctx, current.Name.MustGet().String())
		}
	}
	// if previously was own managed but not anymore, remove the certs and cache
	if wasOwnManagedTLS && !ownManagedTLSIsSet {
		err = d.removeOwnManagedTLS(current)
		if err != nil {
			d.Logger.Warnf("failed to remove own managed TLS", "error", err)
		}
	}
	// if previously was self-signed but not anymore, remove the certs and cache
	if wasSelfSignedTLS && !selfSignedTLSIsSet {
		err = d.removeSelfSignedTLS(current)
		if err != nil {
			d.Logger.Warnf("failed to remove self-signed TLS", "error", err)
		}
	}
	// if previously own managed TLS and now is own managed
	if !wasOwnManagedTLS && ownManagedTLSIsSet {
		if ownManagedTLSKeyIsSet && ownManagedTLSPemIsSet {
			current, err = d.handleOwnManagedTLS(ctx, current)
			if err != nil {
				return fmt.Errorf("faile to handle own managed TLS: %s", err)
			}
		} else {
			return errs.NewValidationError(
				errors.New("Private key and certificate must be provided for own managed TLS"),
			)
		}
	}
	// if previously was own managed TLS
	if wasOwnManagedTLS && ownManagedTLSIsSet {
		// only if both a key and a certificate is provided, overwrite it
		if ownManagedTLSKeyIsSet && ownManagedTLSPemIsSet {
			current, err = d.handleOwnManagedTLS(ctx, current)
			if err != nil {
				return fmt.Errorf("faile to handle own managed TLS: %s", err)
			}
		}
	}
	// if previously not self-signed and now is self-signed
	if !wasSelfSignedTLS && selfSignedTLSIsSet {
		current, err = d.handleSelfSignedTLS(ctx, current)
		if err != nil {
			return fmt.Errorf("failed to handle self-signed TLS: %s", err)
		}
	}
	// when updating, the own managed tls can previous be set with uploaded
	// key and cert, so only if all of them are provided, we handle them
	// update domain
	err = d.DomainRepository.UpdateByID(
		ctx,
		current,
	)
	if err != nil {
		d.Logger.Errorw("failed to update domain by id", "error", err)
		return err
	}
	d.AuditLogAuthorized(ae)
	return nil
}

// DeleteByID deletes a domain by ID
func (d *Domain) DeleteByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
) error {
	return d.deleteDomain(ctx, session, id, false)
}

// deleteDomain is the internal domain deletion method
func (d *Domain) deleteDomain(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	allowProxyDeletion bool,
) error {
	ae := NewAuditEvent("Domain.DeleteByID", session)
	ae.Details["id"] = id.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return err
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return errs.ErrAuthorizationFailed
	}

	// get the domain to check if it's a proxy domain
	current, err := d.DomainRepository.GetByID(ctx, id, &repository.DomainOption{})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		d.Logger.Debugw("domain not found", "error", err)
		return err
	}
	if err != nil {
		d.Logger.Errorw("failed to get domain for deletion", "error", err)
		return err
	}

	// prevent deletion of proxy domains unless explicitly allowed
	if !allowProxyDeletion {
		if domainType, err := current.Type.Get(); err == nil && domainType.String() == "proxy" {
			return validate.WrapErrorWithField(errors.New("proxy domains can only be deleted by deleting the associated proxy configuration"), "domain")
		}
	}

	// prevent deletion of the domain currently serving SCIM provisioning.
	// a missing option means SCIM is not configured; any other read error fails
	// closed so a transient fault cannot let the SCIM domain be deleted.
	scimOpt, scimErr := d.OptionRepository.GetByKey(ctx, data.OptionKeyScimDomain)
	if scimErr != nil && !errors.Is(scimErr, gorm.ErrRecordNotFound) {
		d.Logger.Errorw("failed to check scim domain before deletion", "error", scimErr)
		return scimErr
	}
	if scimErr == nil {
		configuredScimDomain := scimOpt.Value.String()
		if configuredScimDomain != "" {
			if name, nameErr := current.Name.Get(); nameErr == nil && strings.EqualFold(name.String(), configuredScimDomain) {
				return validate.WrapErrorWithField(errors.New("this domain is set as the SCIM provisioning domain; change the SCIM domain in settings before deleting it"), "domain")
			}
		}
	}
	// get the domain
	domain, err := d.DomainRepository.GetByID(
		ctx,
		id,
		&repository.DomainOption{},
	)
	if err != nil {
		return err
	}
	// delete the relation from the campaign templates
	err = d.CampaignTemplateService.RemoveDomainByDomainID(
		ctx,
		session,
		id,
	)
	if err != nil {
		d.Logger.Error("failed to remove domain relation from campaign templates")
		return err
	}
	// delete all asset related to the domain
	err = d.AssetService.DeleteAllByDomainID(
		ctx,
		session,
		id,
	)
	if err != nil {
		d.Logger.Errorw("failed to delete assets related to domain", "error", err)
		return err
	}
	err = d.DomainRepository.DeleteByID(
		ctx,
		id,
	)
	if err != nil {
		d.Logger.Errorw("failed to delete domain by id", "error", err)
		return err
	}
	// clean up if TLS was managed
	if domain.ManagedTLS.MustGet() {
		d.removeManagedDomainTLS(ctx, domain.Name.MustGet().String())
	}
	// clean up if TLS was own managed
	if domain.OwnManagedTLS.MustGet() {
		err = d.removeOwnManagedTLS(domain)
		if err != nil {
			d.Logger.Warnf("failed to remove own managed TLS during deletion", "error", err)
		}
	}
	// clean up if TLS was self-signed
	if domain.SelfSignedTLS.MustGet() {
		err = d.removeSelfSignedTLS(domain)
		if err != nil {
			d.Logger.Warnf("failed to remove self-signed TLS during deletion", "error", err)
		}
	}
	d.AuditLogAuthorized(ae)
	return nil
}

// removeManagedDomainTLS
func (d *Domain) removeManagedDomainTLS(ctx context.Context, domain string) {
	issuerKey := certmagic.DefaultACME.IssuerKey()
	// check if managed certs exists
	sitePrefix := certmagic.StorageKeys.CertsSitePrefix(issuerKey, domain)
	if !d.CertMagicConfig.Storage.Exists(ctx, sitePrefix) {
		d.Logger.Debugw("cache storage does not exist for", "error", sitePrefix)
		return
	}
	// remove pem
	certPath := certmagic.StorageKeys.SiteCert(issuerKey, domain)
	err := d.CertMagicConfig.Storage.Delete(ctx, certPath)
	if err != nil {
		d.Logger.Debugw("attempted to remove managed TLS cert pem", "error", err)
	} else {
		d.Logger.Debugw("removed managed TLS cert pem", "error", certPath)
	}
	// remove .key
	certKey := certmagic.StorageKeys.SitePrivateKey(issuerKey, domain)
	err = d.CertMagicConfig.Storage.Delete(ctx, certKey)
	if err != nil {
		d.Logger.Debugw("attempted to remove managed TLS cert key", "error", err)
	} else {
		d.Logger.Debugw("removed managed TLS cert key", "error", certKey)
	}
	// remove .json info file
	certMeta := certmagic.StorageKeys.SiteMeta(issuerKey, domain)
	err = d.CertMagicConfig.Storage.Delete(ctx, certMeta)
	if err != nil {
		d.Logger.Debugw("attempted to remove managed TLS cert meta", "error", err)
	} else {
		d.Logger.Debugw("removed managed TLS cert meta", "error", certMeta)
	}
	// remove domain cert folder
	err = d.CertMagicConfig.Storage.Delete(ctx, sitePrefix)
	if err != nil {
		d.Logger.Debugw("attempted to remove managed TLS cert folder", "error", err)
	} else {
		d.Logger.Debugw("removed managed TLS folder", "error", sitePrefix)
	}
	// remove from certmagic cache
	certs := d.CertMagicCache.AllMatchingCertificates(domain)
	for _, cert := range certs {
		d.CertMagicCache.Remove([]string{cert.Hash()})
		d.Logger.Debugw("removed cached TLS",
			"domain", domain,
			"hash", cert.Hash(),
		)
	}
}

func (d *Domain) handleOwnManagedTLS(
	ctx context.Context,
	domain *model.Domain) (*model.Domain, error) {
	name := domain.Name.MustGet().String()
	// if the domain is set with self managed TLS
	// upload the pem and key
	key, _ := domain.OwnManagedTLSKey.Get()
	pem, _ := domain.OwnManagedTLSPem.Get()
	if len(key) > 0 && len(pem) > 0 {
		keyBuffer := bytes.NewBufferString(key)
		pemBuffer := bytes.NewBufferString(pem)

		// create root filesystem for secure certificate operations
		root, err := os.OpenRoot(d.OwnManagedCertificatePath)
		if err != nil {
			return nil, fmt.Errorf("failed to open certificate path: %s", err)
		}
		defer root.Close()

		// validate domain name directory access
		_, err = root.Stat(name)
		if err != nil && !os.IsNotExist(err) {
			return nil, fmt.Errorf("invalid domain name for certificate path: %s", err)
		}

		// build path for certificate operations
		// build safe path for certificate operations (validated by OpenRoot)
		// use secure file upload for certificate operations
		err = d.FileService.UploadFile(
			root,
			name+"/cert.key",
			keyBuffer,
			true,
		)
		if err != nil {
			d.Logger.Errorw(
				"failed to upload TLS private key (.key)",
				"error", err,
			)
			return nil, errs.Wrap(err)
		}
		err = d.FileService.UploadFile(
			root,
			name+"/cert.pem",
			pemBuffer,
			true,
		)
		if err != nil {
			d.Logger.Errorw(
				"failed to upload TLS certificate (.pem)",
				"error", err,
			)
			return nil, errs.Wrap(err)
		}
		// evict any previously cached certs for this domain so the stale cert is not served
		existingCerts := d.CertMagicCache.AllMatchingCertificates(name)
		if len(existingCerts) > 0 {
			hashes := make([]string, 0, len(existingCerts))
			for _, c := range existingCerts {
				hashes = append(hashes, c.Hash())
			}
			d.CertMagicCache.Remove(hashes)
			d.Logger.Debugw("evicted stale cached certs before replacing",
				"domain", name,
				"count", len(hashes),
			)
		}
		// Create fresh buffers for caching since upload consumed the original buffers
		keyBufferForCache := bytes.NewBufferString(key)
		pemBufferForCache := bytes.NewBufferString(pem)
		hash, err := d.CertMagicConfig.CacheUnmanagedCertificatePEMBytes(
			ctx,
			pemBufferForCache.Bytes(),
			keyBufferForCache.Bytes(),
			[]string{},
		)
		if err != nil {
			d.Logger.Errorw(
				"failed to cache unmanaged cert for", name,
				"error", err,
			)
			return nil, errs.Wrap(err)
		}
		d.Logger.Debugw("Cached own managed TLS",
			"domain", name,
			"hash", hash,
		)
		domain.OwnManagedTLS = nullable.NewNullableWithValue(true)
		domain.ManagedTLS = nullable.NewNullableWithValue(false)
	} else {
		domain.OwnManagedTLS = nullable.NewNullableWithValue(false)
	}
	return domain, nil
}

func (d *Domain) removeOwnManagedTLS(
	domain *model.Domain,
) error {
	name := domain.Name.MustGet().String()

	// create root filesystem for secure certificate operations
	root, err := os.OpenRoot(d.OwnManagedCertificatePath)
	if err != nil {
		return fmt.Errorf("failed to open certificate path for '%s': %s", name, err)
	}
	defer root.Close()

	// validate domain name directory exists
	_, err = root.Stat(name)
	if err != nil {
		if os.IsNotExist(err) {
			// directory doesn't exist, nothing to delete
			return nil
		}
		return fmt.Errorf("failed to access certificate directory for '%s': %s", name, err)
	}

	// build safe path for deletion (validated by OpenRoot)
	path := filepath.Join(d.OwnManagedCertificatePath, name)
	err = d.FileService.DeleteAll(path)
	if err != nil {
		return fmt.Errorf("failed to delete own managed TLS for '%s' as: %s", name, err)
	}
	d.Logger.Debugw("removed storage for own managed TLS", "name", name)
	certs := d.CertMagicCache.AllMatchingCertificates(name)
	for _, cert := range certs {
		d.CertMagicCache.Remove([]string{cert.Hash()})
		d.Logger.Debugw("removed cached TLS",
			"domain", name,
			"hash", cert.Hash(),
		)
	}
	return nil
}

func (d *Domain) handleSelfSignedTLS(
	ctx context.Context,
	domain *model.Domain) (*model.Domain, error) {
	selfSignedTLS, err := domain.SelfSignedTLS.Get()
	if err != nil || !selfSignedTLS {
		return domain, nil
	}

	name := domain.Name.MustGet().String()

	// create root filesystem for secure certificate operations
	root, err := os.OpenRoot(d.OwnManagedCertificatePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open certificate path: %s", err)
	}
	defer root.Close()

	// validate domain name directory access
	_, err = root.Stat(name)
	if err != nil && !os.IsNotExist(err) {
		return nil, fmt.Errorf("invalid domain name for certificate path: %s", err)
	}

	// build path for certificate operations
	certDir := filepath.Join(d.OwnManagedCertificatePath, name)
	certKeyPath := filepath.Join(certDir, "cert.key")
	certPemPath := filepath.Join(certDir, "cert.pem")

	// generate self-signed certificate
	info := acme.NewInformationWithDefault()
	err = acme.CreateSelfSignedCert(
		d.Logger,
		info,
		[]string{name},
		certPemPath,
		certKeyPath,
	)
	if err != nil {
		d.Logger.Errorw(
			"failed to generate self-signed certificate",
			"domain", name,
			"error", err,
		)
		return nil, errs.Wrap(err)
	}

	// read generated certificate files for caching
	keyBytes, err := os.ReadFile(certKeyPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read generated key file: %s", err)
	}

	pemBytes, err := os.ReadFile(certPemPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read generated cert file: %s", err)
	}

	// cache in certmagic
	hash, err := d.CertMagicConfig.CacheUnmanagedCertificatePEMBytes(
		ctx,
		pemBytes,
		keyBytes,
		[]string{},
	)
	if err != nil {
		d.Logger.Errorw(
			"failed to cache self-signed cert for", name,
			"error", err,
		)
		return nil, errs.Wrap(err)
	}

	d.Logger.Debugw("cached self-signed TLS",
		"domain", name,
		"hash", hash,
	)

	domain.SelfSignedTLS = nullable.NewNullableWithValue(true)
	domain.ManagedTLS = nullable.NewNullableWithValue(false)
	domain.OwnManagedTLS = nullable.NewNullableWithValue(false)

	return domain, nil
}

func (d *Domain) removeSelfSignedTLS(
	domain *model.Domain,
) error {
	name := domain.Name.MustGet().String()

	// create root filesystem for secure certificate operations
	root, err := os.OpenRoot(d.OwnManagedCertificatePath)
	if err != nil {
		return fmt.Errorf("failed to open certificate path for '%s': %s", name, err)
	}
	defer root.Close()

	// validate domain name directory exists
	_, err = root.Stat(name)
	if err != nil {
		if os.IsNotExist(err) {
			// directory doesn't exist, nothing to delete
			return nil
		}
		return fmt.Errorf("failed to access certificate directory for '%s': %s", name, err)
	}

	// build safe path for deletion (validated by OpenRoot)
	path := filepath.Join(d.OwnManagedCertificatePath, name)
	err = d.FileService.DeleteAll(path)
	if err != nil {
		return fmt.Errorf("failed to delete self-signed TLS for '%s' as: %s", name, err)
	}
	d.Logger.Debugw("removed storage for self-signed TLS", "name", name)

	// remove from certmagic cache
	certs := d.CertMagicCache.AllMatchingCertificates(name)
	for _, cert := range certs {
		d.CertMagicCache.Remove([]string{cert.Hash()})
		d.Logger.Debugw("removed cached TLS",
			"domain", name,
			"hash", cert.Hash(),
		)
	}
	return nil
}

// validateProxyDomain validates proxy domain configuration
func (d *Domain) validateProxyDomain(ctx context.Context, domain *model.Domain) error {
	// validate proxy target domain format
	proxyTargetDomain, err := domain.ProxyTargetDomain.Get()
	if err != nil {
		return validate.WrapErrorWithField(errors.New("proxy target domain is required for proxy domains"), "proxyTargetDomain")
	}

	targetDomainStr := proxyTargetDomain.String()
	if targetDomainStr == "" {
		return validate.WrapErrorWithField(errors.New("proxy target domain cannot be empty"), "proxyTargetDomain")
	}

	// validate proxy target format - can be full URL or domain (with optional port)
	if strings.Contains(targetDomainStr, "://") {
		// full URL format - basic validation
		if !strings.HasPrefix(targetDomainStr, "http://") && !strings.HasPrefix(targetDomainStr, "https://") {
			return validate.WrapErrorWithField(errors.New("proxy target domain URL must start with http:// or https://"), "proxyTargetDomain")
		}
	} else {
		// domain-only format (with optional port) - validate as domain, ip address, or localhost
		domainPart := targetDomainStr

		// try to split host and port using net.SplitHostPort for proper handling of ipv6
		host, port, err := net.SplitHostPort(targetDomainStr)
		if err == nil {
			// port was present and successfully split
			domainPart = host

			// validate port is numeric and in valid range
			portNum, err := strconv.Atoi(port)
			if err != nil {
				return validate.WrapErrorWithField(errors.New("port must be numeric in proxy target domain"), "proxyTargetDomain")
			}
			if portNum < 1 || portNum > 65535 {
				return validate.WrapErrorWithField(errors.New("port must be between 1 and 65535"), "proxyTargetDomain")
			}
		}
		// if SplitHostPort fails, targetDomainStr has no port, which is fine

		// check if it's localhost (special case for single-label hostname)
		if strings.ToLower(domainPart) == "localhost" {
			// localhost is always valid
			return nil
		}

		// check if it's an ip address (ipv4 or ipv6)
		if net.ParseIP(domainPart) == nil {
			// not an ip address, validate as domain
			if !isValidDomain(domainPart) {
				return validate.WrapErrorWithField(errors.New("invalid domain format for proxy target domain"), "proxyTargetDomain")
			}
		}
		// if it's a valid ip address, no further validation needed
	}

	return nil
}

// isValidDomain performs basic domain name validation
func isValidDomain(domain string) bool {
	// basic checks - must have at least one dot and valid characters
	if len(domain) == 0 || len(domain) > 253 {
		return false
	}

	// must contain at least one dot
	if !strings.Contains(domain, ".") {
		return false
	}

	// cannot start or end with dash or dot
	if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") ||
		strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
		return false
	}

	// check each label
	labels := strings.Split(domain, ".")
	for _, label := range labels {
		if len(label) == 0 || len(label) > 63 {
			return false
		}

		// label cannot start or end with dash
		if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
			return false
		}

		// basic character check - alphanumeric and dash only
		for _, char := range label {
			if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
				(char >= '0' && char <= '9') || char == '-') {
				return false
			}
		}
	}

	return true
}

// GetByProxyID gets domains by proxy ID
func (d *Domain) GetByProxyID(
	ctx context.Context,
	session *model.Session,
	proxyID *uuid.UUID,
) (*model.Result[model.Domain], error) {
	ae := NewAuditEvent("Domain.GetByProxyID", session)
	ae.Details["proxyID"] = proxyID.String()
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		d.LogAuthError(err)
		return nil, err
	}
	if !isAuthorized {
		d.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	result, err := d.DomainRepository.GetByProxyID(
		ctx,
		proxyID,
		&repository.DomainOption{},
	)
	if err != nil {
		d.Logger.Errorw("failed to get domains by proxy id", "error", err)
		return nil, errs.Wrap(err)
	}
	// no audit on read
	return result, nil
}
