package service

import (
	"context"
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/go-errors/errors"
	"github.com/oapi-codegen/nullable"
	"gopkg.in/yaml.v3"

	"github.com/google/uuid"
	"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"
)

// Proxy is a Proxy service
type Proxy struct {
	Common
	ProxyRepository         *repository.Proxy
	DomainRepository        *repository.Domain
	CampaignRepository      *repository.Campaign
	CampaignTemplateService *CampaignTemplate
	DomainService           *Domain
	ProxySessionManager     *ProxySessionManager
}

// ProxyServiceConfig represents the YAML configuration for proxy
type ProxyServiceConfig struct {
	Proxy  string             `yaml:"proxy,omitempty"`
	Global *ProxyServiceRules `yaml:"global,omitempty"`
}

// ProxyServiceDomainConfig represents configuration for a specific domain mapping
type ProxyServiceDomainConfig struct {
	To          string                       `yaml:"to"`
	Scheme      string                       `yaml:"scheme,omitempty"` // http or https (defaults to https)
	TLS         *ProxyServiceTLSConfig       `yaml:"tls,omitempty"`
	Access      *ProxyServiceAccessControl   `yaml:"access,omitempty"`
	Capture     []ProxyServiceCaptureRule    `yaml:"capture,omitempty"`
	Rewrite     []ProxyServiceReplaceRule    `yaml:"rewrite,omitempty"`
	Response    []ProxyServiceResponseRule   `yaml:"response,omitempty"`
	RewriteURLs []ProxyServiceURLRewriteRule `yaml:"rewrite_urls,omitempty"`
}

// ProxyServiceRules represents capture and replace rules
// ProxyServiceRules represents global rules that apply to all hosts
type ProxyServiceRules struct {
	TLS         *ProxyServiceTLSConfig         `yaml:"tls,omitempty"`
	Access      *ProxyServiceAccessControl     `yaml:"access,omitempty"`
	Impersonate *ProxyServiceImpersonateConfig `yaml:"impersonate,omitempty"`
	Capture     []ProxyServiceCaptureRule      `yaml:"capture,omitempty"`
	Rewrite     []ProxyServiceReplaceRule      `yaml:"rewrite,omitempty"`
	Response    []ProxyServiceResponseRule     `yaml:"response,omitempty"`
	RewriteURLs []ProxyServiceURLRewriteRule   `yaml:"rewrite_urls,omitempty"`
}

// ProxyServiceTLSConfig represents TLS configuration for proxy domains
// TLS modes:
// - "managed": Use Let's Encrypt for automatic certificate management (DEFAULT)
// - "self-signed": Use automatically generated self-signed certificates
//
// Configuration can be set globally and overridden per-host:
//
//	global:
//	  tls:
//	    mode: "managed"
//	example.com:
//	  to: "phishing.com"
//	  tls:
//	    mode: "self-signed"  # override global setting
type ProxyServiceTLSConfig struct {
	Mode string `yaml:"mode"` // "managed" | "self-signed"
}

// ProxyServiceImpersonateConfig represents client impersonation configuration
type ProxyServiceImpersonateConfig struct {
	Enabled  bool `yaml:"enabled"`   // enable surf browser impersonation based on ja4 fingerprint
	RetainUA bool `yaml:"retain_ua"` // retain client's original user-agent instead of using surf's impersonated one
}

// ProxyServiceAccessControl represents access control configuration
type ProxyServiceAccessControl struct {
	Mode   string `yaml:"mode"`              // "public" | "private"
	OnDeny string `yaml:"on_deny,omitempty"` // "404" | "redirect:URL" | status code (only used for private mode)
}

// Access control modes:
// - "public": Allow all traffic (traditional proxy mode) - on_deny is ignored
// - "private": Strict IP-based mode like evilginx2 - whitelist IP after lure access, deny all others (DEFAULT)

// CompilePathPatterns compiles regex patterns for all capture and response rules
func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
	// Compile global capture rule patterns
	if config.Global != nil && config.Global.Capture != nil {
		for i := range config.Global.Capture {
			if err := compileCapturePath(&config.Global.Capture[i]); err != nil {
				return err
			}
		}
	}

	// Compile global response rule patterns
	if config.Global != nil && config.Global.Response != nil {
		for i := range config.Global.Response {
			if err := compileResponsePath(&config.Global.Response[i]); err != nil {
				return err
			}
		}
	}

	// Compile host-specific capture rule patterns
	for _, hostConfig := range config.Hosts {
		if hostConfig != nil && hostConfig.Capture != nil {
			for i := range hostConfig.Capture {
				if err := compileCapturePath(&hostConfig.Capture[i]); err != nil {
					return err
				}
			}
		}
	}

	// Compile host-specific response rule patterns
	for _, hostConfig := range config.Hosts {
		if hostConfig != nil && hostConfig.Response != nil {
			for i := range hostConfig.Response {
				if err := compileResponsePath(&hostConfig.Response[i]); err != nil {
					return err
				}
			}
		}
	}
	return nil
}

// compileResponsePath compiles the path pattern for a response rule
func compileResponsePath(rule *ProxyServiceResponseRule) error {
	if rule.Path != "" {
		pathRe, err := regexp.Compile(rule.Path)
		if err != nil {
			return fmt.Errorf("invalid regex pattern for response path '%s': %w", rule.Path, err)
		}
		rule.PathRe = pathRe
	}
	return nil
}

// compileCapturePath compiles the path pattern for a capture rule
func compileCapturePath(rule *ProxyServiceCaptureRule) error {
	if rule.Path != "" {
		pathRe, err := regexp.Compile(rule.Path)
		if err != nil {
			return fmt.Errorf("invalid regex pattern for path '%s': %w", rule.Path, err)
		}
		rule.PathRe = pathRe
	}
	return nil
}

// ProxyServiceCaptureRule represents a capture rule
type ProxyServiceCaptureRule struct {
	Name     string         `yaml:"name"`
	Method   string         `yaml:"method,omitempty"`
	Path     string         `yaml:"path,omitempty"`
	Find     interface{}    `yaml:"find,omitempty"` // can be string or []string
	Engine   string         `yaml:"engine,omitempty"`
	From     string         `yaml:"from,omitempty"`
	Required *bool          `yaml:"required,omitempty"`
	PathRe   *regexp.Regexp `yaml:"-"` // compiled regex for path matching
}

// GetFindAsStrings returns find field as a slice of strings
func (c *ProxyServiceCaptureRule) GetFindAsStrings() []string {
	if c.Find == nil {
		return []string{}
	}

	switch v := c.Find.(type) {
	case string:
		return []string{v}
	case []interface{}:
		result := make([]string, 0, len(v))
		for _, item := range v {
			if str, ok := item.(string); ok {
				result = append(result, str)
			}
		}
		return result
	case []string:
		return v
	default:
		return []string{}
	}
}

// GetFindAsString returns the first find value as a string
func (c *ProxyServiceCaptureRule) GetFindAsString() string {
	finds := c.GetFindAsStrings()
	if len(finds) > 0 {
		return finds[0]
	}
	return ""
}

// ProxyServiceReplaceRule represents a replacement rule
type ProxyServiceReplaceRule struct {
	Name    string `yaml:"name,omitempty"`
	Engine  string `yaml:"engine,omitempty"`  // "regex" (default) or "dom"
	Find    string `yaml:"find,omitempty"`    // regex pattern (regex engine) or css selector (dom engine)
	Replace string `yaml:"replace,omitempty"` // replacement value for both engines
	Action  string `yaml:"action,omitempty"`  // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove
	Target  string `yaml:"target,omitempty"`  // target matching: "first", "last", "all" (default), "1,3,5", "2-4"
	From    string `yaml:"from,omitempty"`
}

// ProxyServiceURLRewriteRule represents a URL rewrite rule for anti-detection
type ProxyServiceURLRewriteRule struct {
	Find    string                             `yaml:"find"`    // regex pattern to match path
	Replace string                             `yaml:"replace"` // replacement path
	Query   []ProxyServiceURLRewriteQueryParam `yaml:"query,omitempty"`
	Filter  []string                           `yaml:"filter,omitempty"` // query parameters to keep (if empty, keep all)
}

// ProxyServiceURLRewriteQueryParam represents query parameter mapping
type ProxyServiceURLRewriteQueryParam struct {
	Find    string `yaml:"find"`    // original parameter name
	Replace string `yaml:"replace"` // new parameter name
}

// ProxyServiceResponseRule represents a response rule that allows custom responses for specific paths
//
// COMPLETE PROCESSING ORDER & PRECEDENCE:
// The proxy processes rules in this exact order:
//
// REQUEST PROCESSING:
// 1. Response rules (FIRST) - can short-circuit everything
// 2. Session creation/loading (SECOND) - creates session ID internally
// 3. Access control (THIRD) - can block forwarding (uses session existence)
// 4. Capture rules on request (headers, body, cookies)
// 5. Rewrite rules on request (URL params, body patching)
// 6. Request forwarded to target server
//
// RESPONSE PROCESSING:
// 7. Session cookie setting (for new sessions) - cookie sent to client
// 8. Capture rules on response (headers, body, cookies)
// 9. Rewrite rules on response (headers, body, URL replacement)
// 10. Final response returned to client
//
// PRECEDENCE RULES:
// - Response rules with forward: false → skip ALL other processing
// - Response rules with forward: true → capture/rewrite still apply
// - Session created before access control (affects hasSession logic)
// - Access control can block forwarding even with pending response
// - Cookie only set in response phase (not during session creation)
// - Capture rules always run (unless response rule short-circuits)
// - Rewrite rules always run (unless response rule short-circuits)
//
// PRACTICAL EXAMPLES:
//
// Example 1 - Fake API endpoint (response rule wins, bypasses everything):
//
//	response:
//	  - path: "^/api/status$"
//	    body: '{"status": "ok"}'
//	    forward: false
//	access:
//	  mode: "deny"
//	  paths: ["^/api/status$"]
//	capture:
//	  - name: "api_data"
//	    path: "^/api/status$"
//	Result: Returns {"status": "ok"} immediately, no access control, no capture, no forwarding
//
// Example 2 - Monitor + fake response (response + access rules apply):
//
//	response:
//	  - path: "^/api/status$"
//	    body: '{"status": "ok"}'
//	    forward: true
//	access:
//	  mode: "deny"
//	  paths: ["^/api/status$"]
//	capture:
//	  - name: "api_data"
//	    path: "^/api/status$"
//	Result: Creates session → captures request data → returns {"status": "ok"} → sets cookie → NOT forwarded (access blocks it)
//
// Example 3 - Full pipeline (all rules apply):
//
//	response:
//	  - path: "^/api/status$"
//	    body: '{"status": "ok"}'
//	    forward: true
//	access:
//	  mode: "allow"
//	  paths: ["^/api/"]
//	capture:
//	  - name: "api_data"
//	    path: "^/api/status$"
//	rewrite:
//	  - find: "original.com"
//	    replace: "phishing.com"
//	Result: Creates session → captures data → rewrites content → forwards to target → captures response → sets cookie → ignores custom response
//
// RESPONSE RULE FEATURES:
// - forward: false (default) → replace normal proxy behavior
// - forward: true → provide response while still attempting to forward
// - Body content is used as-is (plain text/HTML/JSON/etc.)
type ProxyServiceResponseRule struct {
	Path    string            `yaml:"path"`    // regex pattern for request path
	Status  int               `yaml:"status"`  // HTTP status code (default: 200)
	Headers map[string]string `yaml:"headers"` // response headers to set
	Body    string            `yaml:"body"`    // response body content (supports template variables)
	Forward bool              `yaml:"forward"` // whether to also forward requesto target (default: false)
	PathRe  *regexp.Regexp    `yaml:"-"`       // compiled regex for path matching
}

// ProxyServiceConfigYAML represents the complete YAML configuration structure that matches the actual YAML format
//
// Example YAML configuration with access control and response rules:
//
// version: "0.0"
// global:
//
//	# No access section = private mode by default (secure by default)
//	capture:
//	  - name: "global_navigation"
//	    path: "/important"
//	response:
//	  - path: "^/favicon\\.ico$"
//	    headers:
//	      Content-Type: "image/x-icon"
//	    body: "base64:AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAA=="
//	    forward: false
//
// example.com:
//
//	to: "phishing-example.com"
//	# No access section = private mode by default (secure by default)
//	# To override with public mode or custom deny action:
//	# access:
//	#   mode: "public"                   # Traditional proxy mode
//	# Or:
//	# access:
//	#   mode: "private"
//	#   on_deny: "https://example.com"   # Clean redirect syntax
//	response:
//	  - path: "^/robots\\.txt$"
//	    headers:
//	      Content-Type: "text/plain"
//	    body: |
//	      User-agent: *
//	      Disallow: /
//	    forward: false
//	  - path: "^/api/health$"
//	    headers:
//	      Content-Type: "application/json"
//	    body: '{"status": "ok", "timestamp": "{{timestamp}}"}'
//	    forward: true
//	capture:
//	  - name: "login_capture"
//	    method: "POST"
//	    path: "/login"
//	    find: "password=(.*?)&"
//	    from: "request_body"
//
// Example proxy configuration with client impersonation:
//
//	version: "0.0"
//	proxy: "proxy.example.com:8080"  # optional upstream proxy
//	global:
//	  tls:
//	    mode: "managed"
//	  impersonate:
//	    enabled: true       # enable surf browser impersonation (default: false)
//	    retain_ua: false    # retain client's original user-agent (default: false)
//	example.com:
//	  to: "target.com"
//
// When impersonate.enabled is true, the proxy will:
// - capture the client's ja4 tls fingerprint
// - detect the client's browser (chrome, firefox, safari, edge) and platform (windows, macos, linux, android, ios)
// - use surf library to replicate the exact tls fingerprint, http/2 settings, and header ordering
// - this makes proxied requests appear identical to the original client's browser
// When impersonate.retain_ua is true, the original client user-agent is preserved instead of using surf's impersonated one
type ProxyServiceConfigYAML struct {
	Version string                               `yaml:"version,omitempty"`
	Proxy   string                               `yaml:"proxy,omitempty"`
	Global  *ProxyServiceRules                   `yaml:"global,omitempty"`
	Hosts   map[string]*ProxyServiceDomainConfig `yaml:",inline"` // inline allows domain names as top-level keys
}

// ValidateVersion validates that the version is supported
func ValidateVersion(config *ProxyServiceConfigYAML) error {
	if config.Version != "0.0" {
		return errors.New("only version 0.0 is supported")
	}
	return nil
}

// Create creates a new Proxy
func (m *Proxy) Create(
	ctx context.Context,
	session *model.Session,
	proxy *model.Proxy,
) (*uuid.UUID, error) {
	ae := NewAuditEvent("Proxy.Create", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		m.LogAuthError(err)
		return nil, errs.Wrap(err)
	}
	if !isAuthorized {
		m.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	var companyID *uuid.UUID
	if cid, err := proxy.CompanyID.Get(); err == nil {
		companyID = &cid
	}

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

	// validate Proxy configuration
	if err := m.validateProxyConfig(ctx, proxy); err != nil {
		return nil, err
	}

	// check uniqueness
	name := proxy.Name.MustGet()
	isOK, err := repository.CheckNameIsUnique(
		ctx,
		m.ProxyRepository.DB,
		"proxies",
		name.String(),
		companyID,
		nil,
	)
	if err != nil {
		m.Logger.Errorw("failed to check proxy uniqueness", "error", err)
		return nil, errs.Wrap(err)
	}
	if !isOK {
		m.Logger.Debugw("proxy name is already taken", "name", name.String())
		return nil, validate.WrapErrorWithField(errors.New("is not unique"), "name")
	}

	// create proxy
	id, err := m.ProxyRepository.Insert(
		ctx,
		proxy,
	)
	if err != nil {
		m.Logger.Errorw("failed to create proxy", "error", err)
		return nil, errs.Wrap(err)
	}

	// create associated domains
	err = m.createProxyDomains(ctx, session, id, proxy)
	if err != nil {
		// rollback proxy creation
		m.ProxyRepository.DeleteByID(ctx, id)
		m.Logger.Errorw("failed to create proxy domains", "error", err)
		return nil, errs.Wrap(err)
	}

	ae.Details["id"] = id.String()
	m.AuditLogAuthorized(ae)

	return id, nil
}

// GetAll gets proxies
func (m *Proxy) GetAll(
	ctx context.Context,
	session *model.Session,
	companyID *uuid.UUID,
	options *repository.ProxyOption,
) (*model.Result[model.Proxy], error) {
	result := model.NewEmptyResult[model.Proxy]()
	ae := NewAuditEvent("Proxy.GetAll", session)
	// check permissions
	isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		m.LogAuthError(err)
		return result, errs.Wrap(err)
	}
	if !isAuthorized {
		m.AuditLogNotAuthorized(ae)
		return result, errs.ErrAuthorizationFailed
	}
	result, err = m.ProxyRepository.GetAll(
		ctx,
		companyID,
		options,
	)
	if err != nil {
		m.Logger.Errorw("failed to get proxies", "error", err)
		return result, errs.Wrap(err)
	}
	// no audit log on read
	return result, nil
}

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

// GetByID gets a Proxy by ID
func (m *Proxy) GetByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	options *repository.ProxyOption,
) (*model.Proxy, error) {
	ae := NewAuditEvent("Proxy.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) {
		m.LogAuthError(err)
		return nil, errs.Wrap(err)
	}
	if !isAuthorized {
		m.AuditLogNotAuthorized(ae)
		return nil, errs.ErrAuthorizationFailed
	}
	// get proxy
	proxy, err := m.ProxyRepository.GetByID(
		ctx,
		id,
		options,
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		// return early this is not a an error
		return nil, errs.Wrap(err)
	}
	if err != nil {
		m.Logger.Errorw("failed to get proxy by ID", "error", err)
		return nil, errs.Wrap(err)
	}

	// apply defaults to Proxy configuration for display
	if err := m.applyConfigurationDefaults(proxy); err != nil {
		m.Logger.Errorw("failed to apply configuration defaults", "error", err)
		// don't fail the request, just log the error
	}

	// no audit log on read
	return proxy, nil
}

// UpdateByID updates a Proxy by ID
func (m *Proxy) UpdateByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
	proxy *model.Proxy,
) error {
	ae := NewAuditEvent("Proxy.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) {
		m.LogAuthError(err)
		return err
	}
	if !isAuthorized {
		m.AuditLogNotAuthorized(ae)
		return errs.ErrAuthorizationFailed
	}
	// get current
	current, err := m.ProxyRepository.GetByID(
		ctx,
		id,
		&repository.ProxyOption{},
	)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		m.Logger.Debugw("failed to update proxy by ID", "error", err)
		return err
	}
	if err != nil {
		m.Logger.Errorw("failed to update proxy by ID", "error", err)
		return err
	}
	// update proxy - if a field is present and not null, update it
	if v, err := proxy.Name.Get(); err == nil {
		// check uniqueness
		var companyID *uuid.UUID
		if cid, err := current.CompanyID.Get(); err == nil {
			companyID = &cid
		}
		name := proxy.Name.MustGet()
		isOK, err := repository.CheckNameIsUnique(
			ctx,
			m.ProxyRepository.DB,
			"proxies",
			name.String(),
			companyID,
			id,
		)
		if err != nil {
			m.Logger.Errorw("failed to check proxy uniqueness", "error", err)
			return err
		}
		if !isOK {
			m.Logger.Debugw("proxy name is already taken", "name", name.String())
			return validate.WrapErrorWithField(errors.New("is not unique"), "name")
		}
		current.Name.Set(v)
	}
	if v, err := proxy.Description.Get(); err == nil {
		current.Description.Set(v)
	}
	if v, err := proxy.StartURL.Get(); err == nil {
		current.StartURL.Set(v)
	}
	if v, err := proxy.ProxyConfig.Get(); err == nil {
		current.ProxyConfig.Set(v)
	}

	// validate updated Proxy configuration
	if err := m.validateProxyConfigForUpdate(ctx, current, id); err != nil {
		return err
	}

	// update proxy
	err = m.ProxyRepository.UpdateByID(
		ctx,
		id,
		current,
	)
	if err != nil {
		m.Logger.Errorw("failed to update proxy by ID", "error", err)
		return err
	}

	// update associated domains
	err = m.syncProxyDomains(ctx, session, id, current)
	if err != nil {
		m.Logger.Errorw("failed to sync proxy domains", "error", err)
		return err
	}

	// clear all sessions for this proxy since config has changed
	// this ensures sessions get the new config (capture rules, rewrite rules, etc.)
	if m.ProxySessionManager != nil {
		m.Logger.Debugw("clearing all sessions for updated proxy",
			"proxyID", id.String(),
		)
		m.ProxySessionManager.ClearSessionsForProxy(id.String())
	}

	ae.Details["id"] = id.String()
	m.AuditLogAuthorized(ae)

	return nil
}

// validateProxyConfigForUpdate validates Proxy configuration during update, allowing same domains for same proxy
func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.Proxy, proxyID *uuid.UUID) error {
	// validate Proxy configuration YAML
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return validate.WrapErrorWithField(errors.New("Proxy configuration is required"), "proxyConfig")
	}

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return validate.WrapErrorWithField(errors.New("invalid YAML format: "+err.Error()), "proxyConfig")
	}

	// set default values
	m.setProxyConfigDefaults(&config)

	// compile regex patterns for capture and response rules
	if err := CompilePathPatterns(&config); err != nil {
		return validate.WrapErrorWithField(err, "proxyConfig")
	}

	// validate version (after defaults are applied)
	if err := ValidateVersion(&config); err != nil {
		return validate.WrapErrorWithField(err, "proxyConfig")
	}

	// validate that at least one domain mapping exists
	if len(config.Hosts) == 0 {
		return validate.WrapErrorWithField(errors.New("at least one domain mapping must be specified"), "proxyConfig")
	}

	// validate global uniqueness of capture names across all domains and global rules
	if err := m.validateGlobalCaptureNameUniqueness(&config); err != nil {
		return err
	}

	// ensure that the start URL domain is mentioned in the domain mappings
	startURL, err := proxy.StartURL.Get()
	if err == nil {
		startURLStr := startURL.String()
		var startDomain string

		// extract domain from start URL
		if strings.Contains(startURLStr, "://") {
			// full URL like https://auth.example.com/login
			parts := strings.Split(startURLStr, "://")
			if len(parts) > 1 {
				domainParts := strings.Split(parts[1], "/")
				startDomain = domainParts[0]
			}
		} else if strings.Contains(startURLStr, "/") {
			// domain/path format like auth.example.com/login
			parts := strings.Split(startURLStr, "/")
			startDomain = parts[0]
		} else {
			// just domain like auth.example.com
			startDomain = startURLStr
		}

		// check if start domain is in the domain mappings
		if startDomain != "" {
			found := false
			for originalDomain := range config.Hosts {
				if originalDomain == startDomain {
					found = true
					break
				}
			}
			if !found {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("start URL domain '%s' must be included in domain mappings", startDomain)),
					"proxyConfig",
				)
			}
		}
	}

	// validate each domain mapping
	for originalDomain, domainConfig := range config.Hosts {
		if domainConfig == nil {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("domain config for '%s' is nil", originalDomain)),
				"proxyConfig",
			)
		}

		// validate that 'to' is specified
		if domainConfig.To == "" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("'to' field is required for domain '%s'", originalDomain)),
				"proxyConfig",
			)
		}

		// validate domain-specific access control
		if err := m.validateAccessControl(domainConfig.Access); err != nil {
			return err
		}

		// validate domain-specific capture rules
		if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
			return err
		}

		// validate domain-specific rewrite rules
		if err := m.validateReplaceRules(domainConfig.Rewrite); err != nil {
			return err
		}

		// note: domain uniqueness validation is skipped during updates
		// the syncProxyDomains method will handle domain management properly
	}

	// validate global rules
	if config.Global != nil {
		if err := m.validateAccessControl(config.Global.Access); err != nil {
			return err
		}
		if err := m.validateCaptureRules(config.Global.Capture); err != nil {
			return err
		}
		if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
			return err
		}
		// validate global URL rewrite rules
		if err := m.validateURLRewriteRules(config.Global.RewriteURLs); err != nil {
			return err
		}
		// validate global response rules
		if err := m.validateResponseRules(config.Global.Response); err != nil {
			return err
		}
	}

	return nil
}

// validateCaptureRules validates a slice of capture rules
func (m *Proxy) validateCaptureRules(captureRules []ProxyServiceCaptureRule) error {
	// track capture names to prevent duplicates
	captureNames := make(map[string]bool)

	for _, capture := range captureRules {
		if capture.Name == "" {
			return validate.WrapErrorWithField(errors.New("capture rule name is required"), "proxyConfig")
		}

		// check for duplicate capture names
		if captureNames[capture.Name] {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("duplicate capture rule name '%s' found - each capture rule must have a unique name", capture.Name)),
				"proxyConfig",
			)
		}
		captureNames[capture.Name] = true

		if capture.Path == "" {
			return validate.WrapErrorWithField(errors.New("capture rule path is required"), "proxyConfig")
		}

		// validate engine field
		if capture.Engine != "" {
			validEngines := []string{"regex", "header", "cookie", "json", "form", "urlencoded", "formdata", "multipart"}
			valid := false
			for _, validEngine := range validEngines {
				if capture.Engine == validEngine {
					valid = true
					break
				}
			}
			if !valid {
				return validate.WrapErrorWithField(
					errors.New("invalid 'engine' value in capture rule, must be one of: "+strings.Join(validEngines, ", ")),
					"proxyConfig",
				)
			}
		}

		// allow empty find pattern for any method path-based navigation tracking
		findStr := capture.GetFindAsString()
		isNavigationTracking := capture.Path != "" && findStr == ""

		if findStr == "" && !isNavigationTracking {
			return validate.WrapErrorWithField(
				errors.New("capture rule must have a find pattern, except for path-based navigation tracking"),
				"proxyConfig",
			)
		}

		if findStr != "" {
			engine := capture.Engine
			// backward compatibility: use 'from' to determine engine if not specified
			if engine == "" && capture.From == "cookie" {
				engine = "cookie"
			}
			if engine == "" {
				engine = "regex"
			}

			// validate based on engine type
			switch engine {
			case "regex":
				// validate regex pattern
				if _, err := regexp.Compile(findStr); err != nil {
					return validate.WrapErrorWithField(
						errors.New("invalid regex pattern in capture rule: "+err.Error()),
						"proxyConfig",
					)
				}
			case "header":
				// header engine: find is the header name
				if findStr == "" {
					return validate.WrapErrorWithField(
						errors.New("capture rule with engine='header' must specify header name in 'find' field"),
						"proxyConfig",
					)
				}
			case "cookie":
				// cookie engine: find is the cookie name
				if findStr == "" {
					return validate.WrapErrorWithField(
						errors.New("capture rule with engine='cookie' must specify cookie name in 'find' field"),
						"proxyConfig",
					)
				}
				// validate cookie name format (basic validation)
				cookieName := findStr
				invalidChars := []string{" ", "\t", "\n", "\r", "=", ";", ","}
				for _, char := range invalidChars {
					if strings.Contains(cookieName, char) {
						return validate.WrapErrorWithField(
							errors.New(fmt.Sprintf("cookie name '%s' contains invalid character '%s'", cookieName, char)),
							"proxyConfig",
						)
					}
				}
			case "json":
				// json engine: find is the json path (e.g., "user.name" or "[0].user.name")
				if findStr == "" {
					return validate.WrapErrorWithField(
						errors.New("capture rule with engine='json' must specify JSON path in 'find' field"),
						"proxyConfig",
					)
				}
			case "form", "urlencoded", "formdata", "multipart":
				// form engines: find is the form field name
				if findStr == "" {
					return validate.WrapErrorWithField(
						errors.New(fmt.Sprintf("capture rule with engine='%s' must specify form field name in 'find' field", engine)),
						"proxyConfig",
					)
				}
			}
		}

		// 'from' field defaults to 'any' if not specified (handled in setProxyConfigDefaults)
		// validate 'from' field if specified
		if capture.From != "" {
			validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "cookie", "any"}
			valid := false
			for _, validFrom := range validFromValues {
				if capture.From == validFrom {
					valid = true
					break
				}
			}
			if !valid {
				return validate.WrapErrorWithField(
					errors.New("invalid 'from' value in capture rule, must be one of: "+strings.Join(validFromValues, ", ")),
					"proxyConfig",
				)
			}
		}

		// validate cookie-specific rules (backward compatibility with from='cookie')
		if capture.From == "cookie" {
			findStr := capture.GetFindAsString()
			if findStr == "" {
				return validate.WrapErrorWithField(
					errors.New("capture rule with from='cookie' must specify cookie name in 'find' field"),
					"proxyConfig",
				)
			}

			// method should be specified for cookie captures
			if capture.Method == "" {
				return validate.WrapErrorWithField(
					errors.New("capture rule with from='cookie' should specify HTTP method"),
					"proxyConfig",
				)
			}
		}
	}
	return nil
}

// validateURLRewriteRules validates URL rewrite rules configuration
func (m *Proxy) validateURLRewriteRules(urlRewriteRules []ProxyServiceURLRewriteRule) error {
	for i, rule := range urlRewriteRules {
		// validate find pattern is not empty
		if rule.Find == "" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("URL rewrite rule at index %d must have a find pattern", i)),
				"proxyConfig",
			)
		}

		// validate regex pattern
		if _, err := regexp.Compile(rule.Find); err != nil {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("URL rewrite rule at index %d has invalid regex pattern: %s", i, err.Error())),
				"proxyConfig",
			)
		}

		// validate replace path is not empty
		if rule.Replace == "" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("URL rewrite rule at index %d must have a replace path", i)),
				"proxyConfig",
			)
		}

		// validate query parameter mappings
		queryParamNames := make(map[string]bool)
		for j, queryParam := range rule.Query {
			if queryParam.Find == "" {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("URL rewrite rule at index %d, query param at index %d must have a find parameter name", i, j)),
					"proxyConfig",
				)
			}
			if queryParam.Replace == "" {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("URL rewrite rule at index %d, query param at index %d must have a replace parameter name", i, j)),
					"proxyConfig",
				)
			}

			// check for duplicate query parameter mappings
			if queryParamNames[queryParam.Find] {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("URL rewrite rule at index %d has duplicate query parameter mapping for '%s'", i, queryParam.Find)),
					"proxyConfig",
				)
			}
			queryParamNames[queryParam.Find] = true
		}

		// note: it's valid to both filter and map the same parameter
		// the filter determines which parameters are allowed through,
		// and mappings rename them during the process
	}
	return nil
}

// validateResponseRules validates response rules configuration
func (m *Proxy) validateResponseRules(responseRules []ProxyServiceResponseRule) error {
	for i, rule := range responseRules {
		// validate path is not empty
		if rule.Path == "" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("response rule at index %d must have a path", i)),
				"proxyConfig",
			)
		}

		// validate regex pattern
		if _, err := regexp.Compile(rule.Path); err != nil {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("response rule at index %d has invalid regex pattern '%s': %v", i, rule.Path, err)),
				"proxyConfig",
			)
		}

		// validate status code if specified
		if rule.Status != 0 {
			if rule.Status < 100 || rule.Status > 599 {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("response rule at index %d has invalid status code %d (must be 100-599)", i, rule.Status)),
					"proxyConfig",
				)
			}
		}

		// validate headers
		for headerName, headerValue := range rule.Headers {
			if headerName == "" {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("response rule at index %d has empty header name", i)),
					"proxyConfig",
				)
			}
			if strings.Contains(headerName, ":") || strings.Contains(headerName, "\n") || strings.Contains(headerName, "\r") {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("response rule at index %d has invalid header name '%s'", i, headerName)),
					"proxyConfig",
				)
			}
			if strings.Contains(headerValue, "\n") || strings.Contains(headerValue, "\r") {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("response rule at index %d has invalid header value for '%s'", i, headerName)),
					"proxyConfig",
				)
			}
		}
	}
	return nil
}

// setProxyConfigDefaults sets default values for Proxy configuration after YAML parsing
func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
	// set default version to 0.0 if not specified
	if config.Version == "" {
		config.Version = "0.0"
	}

	// set defaults for global TLS config
	if config.Global != nil && config.Global.TLS != nil {
		// set default mode to managed if not specified
		if config.Global.TLS.Mode == "" {
			config.Global.TLS.Mode = "managed"
		}
	}

	for domain, domainConfig := range config.Hosts {
		// set defaults for domain TLS config
		if domainConfig != nil && domainConfig.TLS != nil {
			// set default mode to managed if not specified
			if domainConfig.TLS.Mode == "" {
				domainConfig.TLS.Mode = "managed"
			}
		}

		if domainConfig != nil && domainConfig.Capture != nil {
			for i := range domainConfig.Capture {
				// set default required to true if not specified
				if domainConfig.Capture[i].Required == nil {
					trueValue := true
					domainConfig.Capture[i].Required = &trueValue
				}
				// set default 'from' to 'any' if not specified
				if domainConfig.Capture[i].From == "" {
					domainConfig.Capture[i].From = "any"
				}
				// set default engine based on from field for backward compatibility
				if domainConfig.Capture[i].Engine == "" {
					if domainConfig.Capture[i].From == "cookie" {
						domainConfig.Capture[i].Engine = "cookie"
					} else {
						domainConfig.Capture[i].Engine = "regex"
					}
				}
			}
		}
		if domainConfig != nil && domainConfig.Response != nil {
			for i := range domainConfig.Response {
				// set default status to 200 if not specified
				if domainConfig.Response[i].Status == 0 {
					domainConfig.Response[i].Status = 200
				}
			}
		}

		// set defaults for domain access control
		if domainConfig != nil && domainConfig.Access != nil {
			// set default mode to private if not specified
			if domainConfig.Access.Mode == "" {
				domainConfig.Access.Mode = "private"
			}
			// set default deny action for private mode if not specified
			if domainConfig.Access.Mode == "private" && domainConfig.Access.OnDeny == "" {
				domainConfig.Access.OnDeny = "404"
			}
		}

		// clean up rewrite rules based on engine type
		if domainConfig.Rewrite != nil {
			for i := range domainConfig.Rewrite {
				m.cleanupRewriteRule(&domainConfig.Rewrite[i])
			}
		}
		config.Hosts[domain] = domainConfig
	}

	// set defaults for global capture rules
	if config.Global != nil && config.Global.Capture != nil {
		for i := range config.Global.Capture {
			// set default required to true if not specified
			if config.Global.Capture[i].Required == nil {
				trueValue := true
				config.Global.Capture[i].Required = &trueValue
			}
			// set default 'from' to 'any' if not specified
			if config.Global.Capture[i].From == "" {
				config.Global.Capture[i].From = "any"
			}
			// set default engine based on from field for backward compatibility
			if config.Global.Capture[i].Engine == "" {
				if config.Global.Capture[i].From == "cookie" {
					config.Global.Capture[i].Engine = "cookie"
				} else {
					config.Global.Capture[i].Engine = "regex"
				}
			}
		}
	}

	// set defaults for global response rules
	if config.Global != nil && config.Global.Response != nil {
		for i := range config.Global.Response {
			// set default status to 200 if not specified
			if config.Global.Response[i].Status == 0 {
				config.Global.Response[i].Status = 200
			}
		}
	}
	// set defaults for global access control
	if config.Global != nil && config.Global.Access != nil {
		// set default mode to private if not specified
		if config.Global.Access.Mode == "" {
			config.Global.Access.Mode = "private"
		}
		// set default deny action for private mode if not specified
		if config.Global.Access.Mode == "private" && config.Global.Access.OnDeny == "" {
			config.Global.Access.OnDeny = "404"
		}
	}

	// clean up global rewrite rules based on engine type
	if config.Global != nil && config.Global.Rewrite != nil {
		for i := range config.Global.Rewrite {
			m.cleanupRewriteRule(&config.Global.Rewrite[i])
		}
	}
}

// cleanupRewriteRule ensures only relevant fields are set based on engine type
func (m *Proxy) cleanupRewriteRule(rule *ProxyServiceReplaceRule) {
	// set default engine to regex if not specified
	if rule.Engine == "" {
		rule.Engine = "regex"
	}

	// clean up fields based on engine type
	if rule.Engine == "regex" {
		// for regex engine, completely remove dom-specific fields
		rule.Action = ""
		rule.Target = ""
		// set default 'from' to 'response_body' for regex if not specified
		if rule.From == "" {
			rule.From = "response_body"
		}
	} else if rule.Engine == "dom" {
		// for dom engine, set default target if not specified
		if rule.Target == "" {
			rule.Target = "all"
		}
		// dom engine always uses response_body, force it
		rule.From = "response_body"
	}
}

// validateReplaceRules validates a slice of replace rules
func (m *Proxy) validateReplaceRules(replaceRules []ProxyServiceReplaceRule) error {
	for _, replace := range replaceRules {
		// set default engine to regex if not specified
		engine := replace.Engine
		if engine == "" {
			engine = "regex"
		}

		// validate engine type
		if engine != "regex" && engine != "dom" {
			return validate.WrapErrorWithField(
				errors.New("invalid 'engine' value in replace rule, must be 'regex' or 'dom'"),
				"proxyConfig",
			)
		}

		// validate based on engine type
		if engine == "regex" {
			if replace.Find == "" {
				return validate.WrapErrorWithField(errors.New("replace rule 'find' is required for regex engine"), "proxyConfig")
			}
			if _, err := regexp.Compile(replace.Find); err != nil {
				return validate.WrapErrorWithField(
					errors.New("invalid regex pattern in replace rule 'find': "+err.Error()),
					"proxyConfig",
				)
			}
		} else if engine == "dom" {
			if replace.Find == "" {
				return validate.WrapErrorWithField(errors.New("replace rule 'find' is required for dom engine"), "proxyConfig")
			}
			if replace.Action == "" {
				return validate.WrapErrorWithField(errors.New("replace rule 'action' is required for dom engine"), "proxyConfig")
			}

			// validate dom actions
			validActions := []string{"setText", "setHtml", "setAttr", "removeAttr", "addClass", "removeClass", "remove"}
			validAction := false
			for _, action := range validActions {
				if replace.Action == action {
					validAction = true
					break
				}
			}
			if !validAction {
				return validate.WrapErrorWithField(
					errors.New("invalid 'action' value in replace rule, must be one of: "+strings.Join(validActions, ", ")),
					"proxyConfig",
				)
			}

			// validate that actions requiring a replace value have one
			if (replace.Action == "setText" || replace.Action == "setHtml" || replace.Action == "setAttr" || replace.Action == "addClass" || replace.Action == "removeClass") && replace.Replace == "" {
				return validate.WrapErrorWithField(
					errors.New("replace rule 'replace' is required for action '"+replace.Action+"'"),
					"proxyConfig",
				)
			}

			// validate target field if specified
			if replace.Target != "" {
				validTargets := []string{"first", "last", "all"}
				isValidTarget := false
				for _, target := range validTargets {
					if replace.Target == target {
						isValidTarget = true
						break
					}
				}
				// also check for numeric patterns like "1,3,5" or "2-4"
				if !isValidTarget {
					if matched, _ := regexp.MatchString(`^(\d+,)*\d+$`, replace.Target); matched {
						isValidTarget = true
					} else if matched, _ := regexp.MatchString(`^\d+-\d+$`, replace.Target); matched {
						isValidTarget = true
					}
				}
				if !isValidTarget {
					return validate.WrapErrorWithField(
						errors.New("invalid 'target' value in replace rule, must be 'first', 'last', 'all', numeric list (1,3,5), or range (2-4)"),
						"proxyConfig",
					)
				}
			}

			// dom engine doesn't use 'from' field - it always works on response_body
			// if 'from' is specified for dom engine, it's ignored (will be forced to response_body)
		}

		if replace.From != "" {
			validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "any"}
			valid := false
			for _, validFrom := range validFromValues {
				if replace.From == validFrom {
					valid = true
					break
				}
			}
			if !valid {
				return validate.WrapErrorWithField(
					errors.New("invalid 'from' value in replace rule, must be one of: "+strings.Join(validFromValues, ", ")),
					"proxyConfig",
				)
			}
		}
	}
	return nil
}

// validateAccessControl validates access control configuration
func (m *Proxy) validateTLSConfig(tlsConfig *ProxyServiceTLSConfig) error {
	if tlsConfig == nil {
		return nil
	}

	// validate TLS mode
	if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" {
		return validate.WrapErrorWithField(
			errors.New("tls.mode must be either 'managed' or 'self-signed'"),
			"proxyConfig",
		)
	}

	return nil
}

func (m *Proxy) validateAccessControl(accessControl *ProxyServiceAccessControl) error {
	if accessControl == nil {
		return nil // access control will be set to defaults
	}

	// set default mode if empty
	if accessControl.Mode == "" {
		accessControl.Mode = "private"
	}

	// validate mode
	if accessControl.Mode != "public" && accessControl.Mode != "private" {
		return validate.WrapErrorWithField(
			errors.New("access control mode must be either 'public' or 'private' - private mode uses IP whitelisting like evilginx2"),
			"proxyConfig",
		)
	}

	// validate deny action (only required for private mode)
	if accessControl.Mode == "private" {
		// set default deny action if empty
		if accessControl.OnDeny == "" {
			accessControl.OnDeny = "404"
		}
		if err := m.validateDenyAction(accessControl.OnDeny); err != nil {
			return err
		}
	}

	return nil
}

// validateDenyAction validates a deny action string
func (m *Proxy) validateDenyAction(action string) error {
	if action == "" {
		return nil // action is optional, will use default
	}

	// check for allow action
	if action == "allow" {
		return nil
	}

	// check for redirect action (auto-detect URLs or old redirect: syntax)
	if strings.HasPrefix(action, "http://") || strings.HasPrefix(action, "https://") {
		if len(action) < 10 { // minimum valid URL length
			return validate.WrapErrorWithField(
				errors.New("redirect URL is too short, must be a valid URL like 'https://example.com'"),
				"proxyConfig",
			)
		}
		return nil
	}

	// check for old redirect: syntax (backwards compatibility)
	if strings.HasPrefix(action, "redirect:") {
		url := strings.TrimPrefix(action, "redirect:")
		if url == "" {
			return validate.WrapErrorWithField(
				errors.New("redirect action must include URL: 'redirect:https://example.com' or just 'https://example.com'"),
				"proxyConfig",
			)
		}
		// basic URL validation for old syntax
		if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
			return validate.WrapErrorWithField(
				errors.New("redirect URL must start with http:// or https://"),
				"proxyConfig",
			)
		}
		return nil
	}

	// check for status code
	if statusCode, err := strconv.Atoi(action); err == nil {
		if statusCode < 100 || statusCode > 599 {
			return validate.WrapErrorWithField(
				errors.New("status code must be between 100 and 599"),
				"proxyConfig",
			)
		}
		return nil
	}

	return validate.WrapErrorWithField(
		errors.New("deny action must be a valid HTTP status code (e.g., '404') or redirect URL (e.g., 'https://example.com')"),
		"proxyConfig",
	)
}

// validateProxyConfig validates Proxy configuration
func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) error {
	// validate Proxy configuration YAML
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return validate.WrapErrorWithField(errors.New("Proxy configuration is required"), "proxyConfig")
	}

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return validate.WrapErrorWithField(errors.New("invalid YAML format: "+err.Error()), "proxyConfig")
	}

	// set default values
	m.setProxyConfigDefaults(&config)

	// compile regex patterns for capture and response rules
	if err := CompilePathPatterns(&config); err != nil {
		return validate.WrapErrorWithField(err, "proxyConfig")
	}

	// validate version (after defaults are applied)
	if err := ValidateVersion(&config); err != nil {
		return validate.WrapErrorWithField(err, "proxyConfig")
	}

	// validate forward proxy configuration
	if err := m.validateForwardProxy(&config); err != nil {
		return validate.WrapErrorWithField(err, "proxyConfig")
	}

	// validate that at least one domain mapping exists
	if len(config.Hosts) == 0 {
		return validate.WrapErrorWithField(errors.New("at least one domain mapping must be specified"), "proxyConfig")
	}

	// validate global uniqueness of capture names across all domains and global rules
	if err := m.validateGlobalCaptureNameUniqueness(&config); err != nil {
		return err
	}

	// ensure that the start URL domain is mentioned in the domain mappings
	startURL, err := proxy.StartURL.Get()
	if err == nil {
		startURLStr := startURL.String()
		var startDomain string

		// extract domain from start URL
		if strings.Contains(startURLStr, "://") {
			// full URL like https://auth.example.com/login
			parts := strings.Split(startURLStr, "://")
			if len(parts) > 1 {
				domainParts := strings.Split(parts[1], "/")
				startDomain = domainParts[0]
			}
		} else if strings.Contains(startURLStr, "/") {
			// domain/path format like auth.example.com/login
			parts := strings.Split(startURLStr, "/")
			startDomain = parts[0]
		} else {
			// just domain like auth.example.com
			startDomain = startURLStr
		}

		// check if start domain is in the domain mappings
		if startDomain != "" {
			found := false
			for originalDomain := range config.Hosts {
				if originalDomain == startDomain {
					found = true
					break
				}
			}
			if !found {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("start URL domain '%s' must be included in domain mappings", startDomain)),
					"proxyConfig",
				)
			}
		}
	}

	// validate each domain mapping
	for originalDomain, domainConfig := range config.Hosts {
		if domainConfig == nil {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("domain config for '%s' is nil", originalDomain)),
				"proxyConfig",
			)
		}

		// validate that 'to' is specified
		if domainConfig.To == "" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("'to' field is required for domain mapping '%s'", originalDomain)),
				"proxyConfig",
			)
		}

		// validate that phishing domain doesn't already exist (unless it's managed by this proxy)
		phishingDomainVO, err := vo.NewString255(domainConfig.To)
		if err != nil {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("invalid phishing domain format: %s", domainConfig.To)),
				"proxyConfig",
			)
		}

		existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if existingDomain != nil {
			// check if this domain is managed by a different proxy or is a regular domain
			if existingDomain.Type.MustGet().String() != "proxy" {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", domainConfig.To)),
					"proxyConfig",
				)
			} else {
				// it's a proxy domain, check if it belongs to a different proxy
				existingTarget, err := existingDomain.ProxyTargetDomain.Get()
				if err == nil {
					startURL, err := proxy.StartURL.Get()
					if err == nil {
						// extract domain from start URL for comparison
						startURLParsed, err := url.Parse(startURL.String())
						if err == nil && existingTarget.String() != startURLParsed.Host {
							return validate.WrapErrorWithField(
								errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration", domainConfig.To)),
								"proxyConfig",
							)
						}
					}
				}
			}
		}

		// validate domain-specific scheme
		if domainConfig.Scheme != "" && domainConfig.Scheme != "http" && domainConfig.Scheme != "https" {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("scheme for domain '%s' must be either 'http' or 'https'", originalDomain)),
				"proxyConfig",
			)
		}

		// validate domain-specific TLS config
		if err := m.validateTLSConfig(domainConfig.TLS); err != nil {
			return err
		}

		// validate domain-specific access control
		if err := m.validateAccessControl(domainConfig.Access); err != nil {
			return err
		}

		// validate domain-specific capture rules
		if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
			return err
		}

		// validate domain-specific capture rules
		if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
			return err
		}

		// validate rewrite rules
		if err := m.validateReplaceRules(domainConfig.Rewrite); err != nil {
			return err
		}

		// validate URL rewrite rules
		if err := m.validateURLRewriteRules(domainConfig.RewriteURLs); err != nil {
			return err
		}

		// validate response rules
		if err := m.validateResponseRules(domainConfig.Response); err != nil {
			return err
		}

		// validate that phishing domain is not used by another proxy
		if err := m.validatePhishingDomainUniquenessByStartURL(ctx, domainConfig.To, proxy.StartURL.MustGet().String()); err != nil {
			return err
		}
	}

	// validate global rules
	if config.Global != nil {
		if err := m.validateTLSConfig(config.Global.TLS); err != nil {
			return err
		}
		if err := m.validateAccessControl(config.Global.Access); err != nil {
			return err
		}
		if err := m.validateCaptureRules(config.Global.Capture); err != nil {
			return err
		}
		if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
			return err
		}
		// validate global URL rewrite rules
		if err := m.validateURLRewriteRules(config.Global.RewriteURLs); err != nil {
			return err
		}
		// validate global response rules
		if err := m.validateResponseRules(config.Global.Response); err != nil {
			return err
		}
	}

	return nil
}

// validateForwardProxy validates the forward proxy configuration
func (m *Proxy) validateForwardProxy(config *ProxyServiceConfigYAML) error {
	if config.Proxy == "" {
		return nil // proxy is optional
	}

	// check if it contains socks4 scheme
	if strings.HasPrefix(config.Proxy, "socks4://") {
		return errors.New("socks4 proxies are not supported. please use socks5:// instead")
	}

	// validate that it can be parsed as a URL if it has a scheme
	if strings.Contains(config.Proxy, "://") {
		parsedURL, err := url.Parse(config.Proxy)
		if err != nil {
			return errors.New("invalid proxy URL format: " + err.Error())
		}

		// validate supported schemes
		scheme := parsedURL.Scheme
		if scheme != "http" && scheme != "https" && scheme != "socks5" {
			return errors.New(fmt.Sprintf("unsupported proxy scheme '%s'. supported schemes: http, https, socks5", scheme))
		}
	}

	return nil
}

// validateGlobalCaptureNameUniqueness ensures all capture rule names are unique across the entire Proxy configuration
func (m *Proxy) validateGlobalCaptureNameUniqueness(config *ProxyServiceConfigYAML) error {
	allCaptureNames := make(map[string]string) // name -> location

	// collect all capture names from domain-specific rules
	for domain, domainConfig := range config.Hosts {
		if domainConfig != nil && domainConfig.Capture != nil {
			for _, capture := range domainConfig.Capture {
				if capture.Name == "" {
					continue // this will be caught by other validation
				}

				if existingLocation, exists := allCaptureNames[capture.Name]; exists {
					return validate.WrapErrorWithField(
						errors.New(fmt.Sprintf("duplicate capture rule name '%s' found in domain '%s' - already used in %s", capture.Name, domain, existingLocation)),
						"proxyConfig",
					)
				}
				allCaptureNames[capture.Name] = fmt.Sprintf("domain '%s'", domain)
			}
		}
	}

	// collect all capture names from global rules
	if config.Global != nil && config.Global.Capture != nil {
		for _, capture := range config.Global.Capture {
			if capture.Name == "" {
				continue // this will be caught by other validation
			}

			if existingLocation, exists := allCaptureNames[capture.Name]; exists {
				return validate.WrapErrorWithField(
					errors.New(fmt.Sprintf("duplicate capture rule name '%s' found in global rules - already used in %s", capture.Name, existingLocation)),
					"proxyConfig",
				)
			}
			allCaptureNames[capture.Name] = "global rules"
		}
	}

	return nil
}

// applyConfigurationDefaults applies default values to Proxy configuration for display
func (m *Proxy) applyConfigurationDefaults(proxy *model.Proxy) error {
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return err
	}

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return err
	}

	// apply defaults
	m.setProxyConfigDefaults(&config)

	// marshal back to YAML
	updatedConfigBytes, err := yaml.Marshal(&config)
	if err != nil {
		return err
	}

	// update the Proxy configuration with defaults applied
	updatedConfigVO := vo.NewString1MBMust(string(updatedConfigBytes))
	proxy.ProxyConfig = nullable.NewNullableWithValue(*updatedConfigVO)
	return nil
}

// deleteProxyDomains deletes all domains associated with a proxy
func (m *Proxy) deleteProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return err
	}

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return err
	}

	// set default values
	m.setProxyConfigDefaults(&config)

	// delete domains for each mapping
	for _, domainConfig := range config.Hosts {
		if domainConfig == nil {
			continue
		}

		// get domain by name and delete if it's a proxy domain
		phishingDomainVO, err := vo.NewString255(domainConfig.To)
		if err != nil {
			continue
		}

		existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
		if err == nil && existingDomain != nil {
			// delete old domains that have proxy type
			if existingDomain.Type.MustGet().String() == "proxy" {
				domainID, err := existingDomain.ID.Get()
				if err == nil {
					err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
					if err != nil {
						m.Logger.Warnw("failed to delete proxy domain",
							"proxyID", proxyID.String(),
							"domain", domainConfig.To,
							"error", err,
						)
					} else {
						m.Logger.Debugw("deleted proxy domain",
							"proxyID", proxyID.String(),
							"domain", domainConfig.To,
						)
					}
				}
			}
		}
	}

	return nil
}

// validatePhishingDomainUniqueness checks if a phishing domain is already used by another proxy
func (m *Proxy) validatePhishingDomainUniqueness(ctx context.Context, phishingDomain string, excludeProxyID *uuid.UUID) error {
	phishingDomainVO, err := vo.NewString255(phishingDomain)
	if err != nil {
		return validate.WrapErrorWithField(
			errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
			"proxyConfig",
		)
	}

	existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	if existingDomain != nil {
		// check if this domain is managed by a different proxy
		if existingDomain.Type.MustGet().String() == "proxy" {
			// check if it's managed by the same proxy we're updating (allowed)
			if excludeProxyID != nil {
				// this is a bit of a workaround - we'd need to track which proxy owns which domain
				// for now, we'll allow updates to existing proxy domains
				// TODO: add a proxy_id field to domains table for proper tracking
				return nil
			}
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("phishing domain '%s' is already used by another proxy", phishingDomain)),
				"proxyConfig",
			)
		} else {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
				"proxyConfig",
			)
		}
	}
	return nil
}

// validatePhishingDomainUniquenessByStartURL checks if a phishing domain is already used by another proxy using start URL comparison
func (m *Proxy) validatePhishingDomainUniquenessByStartURL(ctx context.Context, phishingDomain string, currentStartURL string) error {
	phishingDomainVO, err := vo.NewString255(phishingDomain)
	if err != nil {
		return validate.WrapErrorWithField(
			errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
			"proxyConfig",
		)
	}

	existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	if existingDomain != nil {
		if existingDomain.Type.MustGet().String() == "proxy" {
			// check if it belongs to a different proxy by comparing target domains
			existingTarget, err := existingDomain.ProxyTargetDomain.Get()
			if err == nil {
				// extract domain from current start URL for comparison
				currentStartURLParsed, err := url.Parse(currentStartURL)
				if err != nil {
					return validate.WrapErrorWithField(
						errors.New(fmt.Sprintf("invalid start URL format: %s", currentStartURL)),
						"proxyConfig",
					)
				}

				// normalize and extract domain for comparison
				existingTargetStr := strings.ToLower(strings.TrimSpace(existingTarget.String()))
				currentHostNormalized := strings.ToLower(strings.TrimSpace(currentStartURLParsed.Host))

				// if existing target is a full URL, extract just the host part
				var existingTargetNormalized string
				if strings.Contains(existingTargetStr, "://") {
					// it's a full URL, parse it to get the host
					existingTargetParsed, err := url.Parse(existingTargetStr)
					if err != nil {
						return validate.WrapErrorWithField(
							errors.New(fmt.Sprintf("invalid existing target URL format: %s", existingTargetStr)),
							"proxyConfig",
						)
					}
					existingTargetNormalized = strings.ToLower(strings.TrimSpace(existingTargetParsed.Host))
				} else {
					// it's already just a domain
					existingTargetNormalized = existingTargetStr
				}

				if existingTargetNormalized != currentHostNormalized {
					return validate.WrapErrorWithField(
						errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration", phishingDomain)),
						"proxyConfig",
					)
				}
			}
		} else {
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
				"proxyConfig",
			)
		}
	}
	return nil
}

// validatePhishingDomainUniquenessForUpdate validates phishing domain uniqueness during proxy updates
func (m *Proxy) validatePhishingDomainUniquenessForUpdate(ctx context.Context, phishingDomain string, currentStartURL string, currentProxyID *uuid.UUID) error {
	m.Logger.Debugw("validating phishing domain uniqueness for update",
		"phishingDomain", phishingDomain,
		"currentStartURL", currentStartURL,
		"currentProxyID", currentProxyID.String(),
	)

	phishingDomainVO, err := vo.NewString255(phishingDomain)
	if err != nil {
		m.Logger.Errorw("invalid phishing domain format",
			"phishingDomain", phishingDomain,
			"error", err,
		)
		return validate.WrapErrorWithField(
			errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
			"proxyConfig",
		)
	}

	existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		m.Logger.Errorw("error getting existing domain",
			"phishingDomain", phishingDomain,
			"error", err,
		)
		return err
	}

	if existingDomain == nil {
		m.Logger.Debugw("no existing domain found, validation passed",
			"phishingDomain", phishingDomain,
		)
		return nil
	}

	m.Logger.Debugw("existing domain found",
		"phishingDomain", phishingDomain,
		"existingDomainType", existingDomain.Type.MustGet().String(),
	)

	if existingDomain.Type.MustGet().String() == "proxy" {
		// check if it belongs to a different proxy by comparing target domains
		existingTarget, err := existingDomain.ProxyTargetDomain.Get()
		if err != nil {
			m.Logger.Errorw("error getting existing domain proxy target",
				"phishingDomain", phishingDomain,
				"error", err,
			)
			return err
		}

		m.Logger.Debugw("existing domain proxy target found",
			"phishingDomain", phishingDomain,
			"existingTarget", existingTarget.String(),
		)

		// extract domain from current start URL for comparison
		currentStartURLParsed, err := url.Parse(currentStartURL)
		if err != nil {
			m.Logger.Errorw("error parsing current start URL",
				"currentStartURL", currentStartURL,
				"error", err,
			)
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("invalid start URL format: %s", currentStartURL)),
				"proxyConfig",
			)
		}

		// normalize and extract domain for comparison
		existingTargetStr := strings.ToLower(strings.TrimSpace(existingTarget.String()))
		currentHostNormalized := strings.ToLower(strings.TrimSpace(currentStartURLParsed.Host))

		// if existing target is a full URL, extract just the host part
		var existingTargetNormalized string
		if strings.Contains(existingTargetStr, "://") {
			// it's a full URL, parse it to get the host
			existingTargetParsed, err := url.Parse(existingTargetStr)
			if err != nil {
				m.Logger.Errorw("error parsing existing target URL",
					"existingTarget", existingTargetStr,
					"error", err,
				)
				return err
			}
			existingTargetNormalized = strings.ToLower(strings.TrimSpace(existingTargetParsed.Host))
		} else {
			// it's already just a domain
			existingTargetNormalized = existingTargetStr
		}

		m.Logger.Debugw("comparing normalized domains",
			"phishingDomain", phishingDomain,
			"existingTargetStr", existingTargetStr,
			"existingTargetNormalized", existingTargetNormalized,
			"currentHostNormalized", currentHostNormalized,
		)

		// if target domains don't match, it belongs to a different proxy
		if existingTargetNormalized != currentHostNormalized {
			m.Logger.Warnw("phishing domain belongs to different proxy",
				"phishingDomain", phishingDomain,
				"existingTargetNormalized", existingTargetNormalized,
				"currentHostNormalized", currentHostNormalized,
				"currentProxyID", currentProxyID.String(),
			)
			return validate.WrapErrorWithField(
				errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration (existing target: %s, current target: %s)", phishingDomain, existingTargetNormalized, currentHostNormalized)),
				"proxyConfig",
			)
		}

		// if target domains match, this domain belongs to the current proxy being updated, so it's allowed
		m.Logger.Debugw("phishing domain belongs to current proxy, allowing reuse",
			"domain", phishingDomain,
			"proxyID", currentProxyID.String(),
			"existingTarget", existingTargetNormalized,
			"currentHost", currentHostNormalized,
		)
	} else {
		m.Logger.Warnw("domain exists as regular domain, not proxy",
			"phishingDomain", phishingDomain,
			"existingDomainType", existingDomain.Type.MustGet().String(),
		)
		return validate.WrapErrorWithField(
			errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
			"proxyConfig",
		)
	}
	return nil
}

// createProxyDomains creates domains for the proxy based on the configuration
func (m *Proxy) createProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return errs.NewCustomError(fmt.Errorf("failed to get proxy config: %w", err))
	}

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return errs.NewCustomError(fmt.Errorf("failed to parse proxy config YAML: %w", err))
	}

	// set default values
	m.setProxyConfigDefaults(&config)

	var companyID *uuid.UUID
	if cid, err := proxy.CompanyID.Get(); err == nil {
		companyID = &cid
	}

	startURL := proxy.StartURL.MustGet()
	createdDomains := make([]string, 0)

	// create domains for each mapping
	for originalDomain, domainConfig := range config.Hosts {
		if domainConfig == nil {
			continue
		}

		if domainConfig.To == "" {
			m.Logger.Warnw("empty 'to' field in domain config",
				"proxyID", proxyID.String(),
				"originalDomain", originalDomain,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("'to' field is required for domain mapping '%s'", originalDomain))
		}

		// check if domain already exists (might be from previous failed attempt)
		phishingDomainVO, err := vo.NewString255(domainConfig.To)
		if err != nil {
			m.Logger.Warnw("invalid phishing domain format",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("invalid phishing domain format %s: %w", domainConfig.To, err))
		}

		existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
		if err == nil && existingDomain != nil {
			// domain already exists, check if it's compatible
			if existingDomain.Type.MustGet().String() == "proxy" {
				existingTarget, err := existingDomain.ProxyTargetDomain.Get()
				if err == nil && existingTarget.String() == startURL.String() {
					// compatible existing domain, skip creation
					m.Logger.Debugw("proxy domain already exists, skipping creation",
						"proxyID", proxyID.String(),
						"domain", domainConfig.To,
					)
					createdDomains = append(createdDomains, domainConfig.To)
					continue
				}
			}
			// incompatible domain exists
			m.Logger.Warnw("incompatible domain already exists",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"existingType", existingDomain.Type.MustGet().String(),
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("domain %s already exists and is incompatible", domainConfig.To))
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			// database error
			m.Logger.Errorw("failed to check existing domain",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return fmt.Errorf("failed to check existing domain %s: %w", domainConfig.To, err)
		}

		// create new domain
		domain := &model.Domain{}
		domain.Name.Set(*vo.NewString255Must(domainConfig.To))
		domain.Type.Set(*vo.NewString32Must("proxy"))
		domain.ProxyID.Set(*proxyID)

		// set the target domain to the original domain from the YAML config
		proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
		if err != nil {
			m.Logger.Errorw("failed to create proxy target domain",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"startURL", startURL.String(),
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("failed to create proxy target domain for %s: %w", domainConfig.To, err))
		}
		domain.ProxyTargetDomain.Set(*proxyTargetDomain)

		// determine TLS mode from config (check domain-specific first, then global, then default to managed)
		tlsMode := "managed" // default
		if domainConfig.TLS != nil && domainConfig.TLS.Mode != "" {
			tlsMode = domainConfig.TLS.Mode
		} else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" {
			tlsMode = config.Global.TLS.Mode
		}

		domain.HostWebsite.Set(false)
		if tlsMode == "self-signed" {
			domain.ManagedTLS.Set(false)
			domain.OwnManagedTLS.Set(false)
			domain.SelfSignedTLS.Set(true)
		} else {
			// default to managed
			domain.ManagedTLS.Set(true)
			domain.OwnManagedTLS.Set(false)
			domain.SelfSignedTLS.Set(false)
		}

		pageContent, err := vo.NewOptionalString1MB("")
		if err != nil {
			m.Logger.Errorw("failed to create page content",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("failed to create page content for %s: %w", domainConfig.To, err))
		}
		domain.PageContent.Set(*pageContent)

		pageNotFoundContent, err := vo.NewOptionalString1MB("")
		if err != nil {
			m.Logger.Errorw("failed to create page not found content",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("failed to create page not found content for %s: %w", domainConfig.To, err))
		}
		domain.PageNotFoundContent.Set(*pageNotFoundContent)

		redirectURL, err := vo.NewOptionalString1024("")
		if err != nil {
			m.Logger.Errorw("failed to create redirect URL",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("failed to create redirect URL for %s: %w", domainConfig.To, err))
		}
		domain.RedirectURL.Set(*redirectURL)

		if companyID != nil {
			domain.CompanyID.Set(*companyID)
		}

		_, err = m.DomainService.CreateProxyDomain(ctx, session, domain)
		if err != nil {
			m.Logger.Errorw("failed to create proxy domain",
				"proxyID", proxyID.String(),
				"domain", domainConfig.To,
				"error", err,
			)
			// rollback created domains on error
			m.rollbackCreatedDomains(ctx, session, createdDomains)
			return errs.NewCustomError(fmt.Errorf("failed to create domain %s: %w", domainConfig.To, err))
		}

		createdDomains = append(createdDomains, domainConfig.To)
		m.Logger.Debugw("created proxy domain",
			"proxyID", proxyID.String(),
			"domain", domainConfig.To,
		)
	}

	m.Logger.Infow("successfully created all proxy domains",
		"proxyID", proxyID.String(),
		"domainsCreated", len(createdDomains),
		"domains", createdDomains,
	)

	return nil
}

// rollbackCreatedDomains attempts to delete domains that were created during a failed proxy creation
func (m *Proxy) rollbackCreatedDomains(ctx context.Context, session *model.Session, createdDomains []string) {
	for _, domainName := range createdDomains {
		phishingDomainVO, err := vo.NewString255(domainName)
		if err != nil {
			m.Logger.Warnw("failed to create domain VO for rollback",
				"domain", domainName,
				"error", err,
			)
			continue
		}

		existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
		if err != nil {
			m.Logger.Warnw("failed to get domain for rollback",
				"domain", domainName,
				"error", err,
			)
			continue
		}

		if existingDomain != nil && existingDomain.Type.MustGet().String() == "proxy" {
			domainID, err := existingDomain.ID.Get()
			if err == nil {
				err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
				if err != nil {
					m.Logger.Warnw("failed to rollback proxy domain",
						"domain", domainName,
						"error", err,
					)
				} else {
					m.Logger.Debugw("rolled back proxy domain",
						"domain", domainName,
					)
				}
			}
		}
	}
}

// syncProxyDomains synchronizes domains for the proxy based on the configuration
func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
	proxyConfig, err := proxy.ProxyConfig.Get()
	if err != nil {
		return errs.NewCustomError(fmt.Errorf("failed to get proxy config for sync: %w", err))
	}

	// get current proxy domains by proxy ID
	currentDomainsResult, err := m.DomainService.GetByProxyID(ctx, session, proxyID)
	if err != nil {
		return fmt.Errorf("failed to get current proxy domains: %w", err)
	}

	// collect specific errors for better reporting
	var syncErrors []string

	currentDomains := make(map[string]*model.Domain)
	for _, domain := range currentDomainsResult.Rows {
		currentDomains[domain.Name.MustGet().String()] = domain
	}

	m.Logger.Debugw("found existing proxy domains for sync",
		"proxyID", proxyID.String(),
		"currentDomainCount", len(currentDomains),
		"currentDomains", func() []string {
			domains := make([]string, 0, len(currentDomains))
			for name := range currentDomains {
				domains = append(domains, name)
			}
			return domains
		}(),
	)

	// parse complete YAML structure
	var config ProxyServiceConfigYAML
	if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
		return errs.NewCustomError(fmt.Errorf("failed to parse proxy config YAML for sync: %w", err))
	}

	// set default values
	m.setProxyConfigDefaults(&config)

	// get desired domains from config
	desiredDomains := make(map[string]string) // phishing domain -> original domain
	for originalDomain, domainConfig := range config.Hosts {
		if domainConfig == nil {
			continue
		}

		if domainConfig.To != "" {
			desiredDomains[domainConfig.To] = originalDomain
		}
	}

	m.Logger.Debugw("parsed desired domains from config",
		"proxyID", proxyID.String(),
		"desiredDomainCount", len(desiredDomains),
		"desiredDomains", func() []string {
			domains := make([]string, 0, len(desiredDomains))
			for name := range desiredDomains {
				domains = append(domains, name)
			}
			return domains
		}(),
	)

	// delete domains that are no longer needed
	deletedCount := 0
	for phishingDomain, domain := range currentDomains {
		if _, exists := desiredDomains[phishingDomain]; !exists {
			m.Logger.Debugw("domain marked for deletion",
				"proxyID", proxyID.String(),
				"domain", phishingDomain,
			)
			domainID, err := domain.ID.Get()
			if err == nil {
				err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
				if err != nil {
					m.Logger.Warnw("failed to delete removed proxy domain",
						"proxyID", proxyID.String(),
						"domain", phishingDomain,
						"error", err,
					)
				} else {
					m.Logger.Infow("deleted removed proxy domain",
						"proxyID", proxyID.String(),
						"domain", phishingDomain,
					)
					deletedCount++
				}
			} else {
				m.Logger.Warnw("failed to get domain ID for deletion",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"error", err,
				)
			}
		} else {
			m.Logger.Debugw("domain still needed, keeping",
				"proxyID", proxyID.String(),
				"domain", phishingDomain,
			)
		}
	}

	// create or update domains that are needed
	createdCount := 0
	updatedCount := 0
	errorCount := 0

	for phishingDomain, originalDomain := range desiredDomains {
		if existingDomain, exists := currentDomains[phishingDomain]; exists {
			// domain already exists, check if target domain needs updating
			needsUpdate := false
			currentTarget, err := existingDomain.ProxyTargetDomain.Get()

			if err != nil || currentTarget.String() != originalDomain {
				// update the target domain
				proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
				if err == nil {
					existingDomain.ProxyTargetDomain.Set(*proxyTargetDomain)
					needsUpdate = true
				} else {
					m.Logger.Warnw("failed to create proxy target domain for update",
						"proxyID", proxyID.String(),
						"domain", phishingDomain,
						"error", err,
					)
					errorCount++
					continue
				}
			}

			// check if TLS configuration needs updating
			tlsMode := "managed" // default
			if hostConfig, exists := config.Hosts[originalDomain]; exists && hostConfig != nil && hostConfig.TLS != nil && hostConfig.TLS.Mode != "" {
				tlsMode = hostConfig.TLS.Mode
			} else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" {
				tlsMode = config.Global.TLS.Mode
			}

			// check current TLS settings
			currentManagedTLS := existingDomain.ManagedTLS.MustGet()
			currentSelfSignedTLS := existingDomain.SelfSignedTLS.MustGet()

			// determine if TLS settings need updating
			if tlsMode == "self-signed" && !currentSelfSignedTLS {
				existingDomain.ManagedTLS.Set(false)
				existingDomain.OwnManagedTLS.Set(false)
				existingDomain.SelfSignedTLS.Set(true)
				needsUpdate = true
			} else if tlsMode == "managed" && !currentManagedTLS {
				existingDomain.ManagedTLS.Set(true)
				existingDomain.OwnManagedTLS.Set(false)
				existingDomain.SelfSignedTLS.Set(false)
				needsUpdate = true
			}

			if needsUpdate {
				domainID, err := existingDomain.ID.Get()
				if err == nil {
					err = m.DomainService.UpdateProxyDomain(ctx, session, &domainID, existingDomain)
					if err != nil {
						m.Logger.Warnw("failed to update existing proxy domain",
							"proxyID", proxyID.String(),
							"domain", phishingDomain,
							"error", err,
						)
						errorCount++
					} else {
						m.Logger.Debugw("updated existing proxy domain",
							"proxyID", proxyID.String(),
							"domain", phishingDomain,
						)
						updatedCount++
					}
				} else {
					m.Logger.Warnw("failed to get domain ID for update",
						"proxyID", proxyID.String(),
						"domain", phishingDomain,
						"error", err,
					)
					errorCount++
				}
			}
		} else {
			// create new domain
			domain := &model.Domain{}
			domain.Name.Set(*vo.NewString255Must(phishingDomain))
			domain.Type.Set(*vo.NewString32Must("proxy"))
			domain.ProxyID.Set(*proxyID)

			proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
			if err != nil {
				m.Logger.Warnw("failed to create proxy target domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"originalDomain", originalDomain,
					"error", err,
				)
				errorCount++
				continue
			}
			domain.ProxyTargetDomain.Set(*proxyTargetDomain)

			// determine TLS mode from config (check domain-specific first, then global, then default to managed)
			tlsMode := "managed" // default
			if hostConfig, exists := config.Hosts[originalDomain]; exists && hostConfig != nil && hostConfig.TLS != nil && hostConfig.TLS.Mode != "" {
				tlsMode = hostConfig.TLS.Mode
			} else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" {
				tlsMode = config.Global.TLS.Mode
			}

			domain.HostWebsite.Set(false)
			if tlsMode == "self-signed" {
				domain.ManagedTLS.Set(false)
				domain.OwnManagedTLS.Set(false)
				domain.SelfSignedTLS.Set(true)
			} else {
				// default to managed
				domain.ManagedTLS.Set(true)
				domain.OwnManagedTLS.Set(false)
				domain.SelfSignedTLS.Set(false)
			}

			pageContent, err := vo.NewOptionalString1MB("")
			if err != nil {
				m.Logger.Warnw("failed to create page content for proxy domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"error", err,
				)
				errorCount++
				continue
			}
			domain.PageContent.Set(*pageContent)

			pageNotFoundContent, err := vo.NewOptionalString1MB("")
			if err != nil {
				m.Logger.Warnw("failed to create page not found content for proxy domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"error", err,
				)
				errorCount++
				continue
			}
			domain.PageNotFoundContent.Set(*pageNotFoundContent)

			redirectURL, err := vo.NewOptionalString1024("")
			if err != nil {
				errMsg := fmt.Sprintf("failed to create redirect URL for domain '%s': %v", phishingDomain, err)
				m.Logger.Warnw("failed to create redirect URL for proxy domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"error", err,
				)
				syncErrors = append(syncErrors, errMsg)
				errorCount++
				continue
			}
			domain.RedirectURL.Set(*redirectURL)

			var companyID *uuid.UUID
			if cid, err := proxy.CompanyID.Get(); err == nil {
				companyID = &cid
				domain.CompanyID.Set(*companyID)
			}

			_, err = m.DomainService.CreateProxyDomain(ctx, session, domain)
			if err != nil {
				errMsg := fmt.Sprintf("failed to create domain '%s': %v", phishingDomain, err)
				m.Logger.Warnw("failed to create new proxy domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
					"error", err,
				)
				syncErrors = append(syncErrors, errMsg)
				errorCount++
			} else {
				m.Logger.Debugw("created new proxy domain",
					"proxyID", proxyID.String(),
					"domain", phishingDomain,
				)
				createdCount++
			}
		}
	}

	m.Logger.Infow("completed proxy domain synchronization",
		"proxyID", proxyID.String(),
		"domainsDeleted", deletedCount,
		"domainsCreated", createdCount,
		"domainsUpdated", updatedCount,
		"errors", errorCount,
	)

	if errorCount > 0 {
		errorDetails := strings.Join(syncErrors, "; ")
		return errs.NewCustomError(fmt.Errorf("proxy domain sync failed with %d errors: %s", errorCount, errorDetails))
	}

	return nil
}

// DeleteByID deletes a proxy by ID
func (m *Proxy) DeleteByID(
	ctx context.Context,
	session *model.Session,
	id *uuid.UUID,
) error {
	ae := NewAuditEvent("Proxy.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) {
		m.LogAuthError(err)
		return err
	}
	if !isAuthorized {
		m.AuditLogNotAuthorized(ae)
		return errs.ErrAuthorizationFailed
	}

	// get current proxy before deletion to access its domains
	current, err := m.ProxyRepository.GetByID(ctx, id, &repository.ProxyOption{})
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		m.Logger.Errorw("failed to get proxy for domain cleanup", "error", err)
		return err
	}

	// delete associated proxy domains
	if current != nil {
		err = m.deleteProxyDomains(ctx, session, id, current)
		if err != nil {
			m.Logger.Errorw("failed to delete proxy domains", "error", err)
			// continue with proxy deletion even if domain cleanup fails
		}
	}

	// remove the relation from campaign templates
	err = m.CampaignTemplateService.RemoveProxiesByProxyID(
		ctx,
		session,
		id,
	)
	if err != nil {
		m.Logger.Errorw("failed to remove proxy ID relations from campaign templates", "error", err)
		return err
	}

	// delete proxy
	err = m.ProxyRepository.DeleteByID(
		ctx,
		id,
	)
	if err != nil {
		m.Logger.Errorw("failed to delete proxy by ID", "error", err)
		return err
	}
	m.AuditLogAuthorized(ae)

	return nil
}
