package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/example/sessionguard/internal/model"
)

type Master struct {
	Listen              string                 `json:"listen"`
	PublicURL           string                 `json:"public_url"`
	DataFile            string                 `json:"data_file,omitempty"`
	DatabaseURL         string                 `json:"database_url,omitempty"`
	EnrollmentToken     string                 `json:"enrollment_token"`
	OIDC                model.OIDCConfig       `json:"oidc"`
	AccessAuth          model.AccessAuthConfig `json:"access_auth"`
	RBAC                model.RBACConfig       `json:"rbac"`
	Broker              model.BrokerConfig     `json:"broker"`
	Alerts              model.AlertConfig      `json:"alerts"`
	OfflineAfterSeconds int                    `json:"offline_after_seconds"`
	HistoryLimit        int                    `json:"history_limit"`
}

type Agent struct {
	Listen           string           `json:"listen"`
	PublicURL        string           `json:"public_url"`
	DataDir          string           `json:"data_dir"`
	MasterURL        string           `json:"master_url"`
	EnrollmentToken  string           `json:"enrollment_token"`
	HeartbeatSeconds int              `json:"heartbeat_seconds"`
	OIDC             model.OIDCConfig `json:"oidc"`
	Policy           model.Policy     `json:"policy"`
}

func LoadMaster(path string) (Master, error) {
	var c Master
	if err := read(path, &c); err != nil {
		return c, err
	}
	applyMasterEnv(&c)
	if c.Listen == "" {
		c.Listen = ":8080"
	}
	if c.DataFile == "" {
		c.DataFile = "./data/master.json"
	}
	if c.OfflineAfterSeconds <= 0 {
		c.OfflineAfterSeconds = 30
	}
	if c.HistoryLimit <= 0 {
		c.HistoryLimit = 50000
	}
	if c.Broker.LeaseSeconds <= 0 {
		c.Broker.LeaseSeconds = 900
	}
	if c.Broker.MinHealthScore <= 0 {
		c.Broker.MinHealthScore = 60
	}
	if c.Alerts.CPUPercent <= 0 {
		c.Alerts.CPUPercent = 90
	}
	if c.Alerts.MemoryPercent <= 0 {
		c.Alerts.MemoryPercent = 90
	}
	if c.Alerts.DiskFreeGB <= 0 {
		c.Alerts.DiskFreeGB = 10
	}
	if c.Alerts.HealthScore <= 0 {
		c.Alerts.HealthScore = 50
	}
	if c.Alerts.OfflineSeconds <= 0 {
		c.Alerts.OfflineSeconds = 120
	}
	if c.Alerts.ProfileFailures <= 0 {
		c.Alerts.ProfileFailures = 3
	}
	if c.Alerts.DisconnectedSessions <= 0 {
		c.Alerts.DisconnectedSessions = 20
	}
	if c.Alerts.LogonDurationSeconds <= 0 {
		c.Alerts.LogonDurationSeconds = 30
	}
	if c.Alerts.NotificationMinInterval <= 0 {
		c.Alerts.NotificationMinInterval = 900
	}
	if c.RBAC.DefaultRole == "" {
		c.RBAC.DefaultRole = "viewer"
	}
	if c.RBAC.Groups == nil {
		c.RBAC.Groups = map[string][]string{}
	}
	if c.AccessAuth.Enabled {
		if strings.TrimSpace(c.AccessAuth.Issuer) == "" {
			c.AccessAuth.Issuer = c.OIDC.Issuer
		}
		if strings.TrimSpace(c.AccessAuth.ClientID) == "" {
			c.AccessAuth.ClientID = c.OIDC.ClientID
		}
		if strings.TrimSpace(c.AccessAuth.ClientSecret) == "" {
			c.AccessAuth.ClientSecret = c.OIDC.ClientSecret
		}
		if c.AccessAuth.CookieName == "" {
			c.AccessAuth.CookieName = "sg_access_session"
		}
		if c.AccessAuth.SessionHours <= 0 {
			c.AccessAuth.SessionHours = 8
		}
		if c.AccessAuth.UsernameClaim == "" {
			c.AccessAuth.UsernameClaim = "preferred_username"
		}
		if err := validateAccessAuth(c.AccessAuth); err != nil {
			return c, err
		}
	}
	if err := validateOIDC(c.OIDC); err != nil {
		return c, err
	}
	if c.Broker.Enabled && strings.TrimSpace(c.Broker.APIKey) == "" {
		return c, errors.New("broker.api_key is required when broker is enabled")
	}
	return c, nil
}

func LoadAgent(path string) (Agent, error) {
	var c Agent
	if err := read(path, &c); err != nil {
		return c, err
	}
	applyAgentEnv(&c)
	if c.Listen == "" {
		c.Listen = ":9091"
	}
	if c.DataDir == "" {
		c.DataDir = `C:\ProgramData\SessionGuard`
	}
	if c.HeartbeatSeconds <= 0 {
		c.HeartbeatSeconds = 10
	}
	if c.Policy.Cleanup.GraceSeconds <= 0 {
		c.Policy.Cleanup.GraceSeconds = 600
	}
	if c.Policy.Cleanup.PollSeconds <= 0 {
		c.Policy.Cleanup.PollSeconds = 10
	}
	if c.Policy.Cleanup.RetrySeconds <= 0 {
		c.Policy.Cleanup.RetrySeconds = 60
	}
	if len(c.Policy.Cleanup.AllowedProfileRoots) == 0 {
		c.Policy.Cleanup.AllowedProfileRoots = []string{`C:\Users`}
	}
	if c.Policy.Cleanup.ExcludeUsers == nil {
		c.Policy.Cleanup.ExcludeUsers = []string{"Administrator", "DefaultAccount", "WDAGUtilityAccount"}
	}
	if c.Policy.Cleanup.ExcludeSIDs == nil {
		c.Policy.Cleanup.ExcludeSIDs = []string{"S-1-5-18", "S-1-5-19", "S-1-5-20"}
	}
	NormalizePolicy(&c.Policy)
	if err := ValidatePolicy(c.Policy); err != nil {
		return c, err
	}
	if c.OIDC.Issuer != "" {
		if err := validateOIDC(c.OIDC); err != nil {
			return c, err
		}
	}
	return c, nil
}

func applyMasterEnv(c *Master) {
	set := func(name string, dst *string) {
		if v := strings.TrimSpace(os.Getenv(name)); v != "" {
			*dst = v
		}
	}
	set("SESSIONGUARD_DATABASE_URL", &c.DatabaseURL)
	set("SESSIONGUARD_ENROLLMENT_TOKEN", &c.EnrollmentToken)
	set("SESSIONGUARD_BROKER_API_KEY", &c.Broker.APIKey)
	set("SESSIONGUARD_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret)
	set("SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET", &c.AccessAuth.ClientSecret)
	set("SESSIONGUARD_ALERT_WEBHOOK_URL", &c.Alerts.WebhookURL)
}

func applyAgentEnv(c *Agent) {
	set := func(name string, dst *string) {
		if v := strings.TrimSpace(os.Getenv(name)); v != "" {
			*dst = v
		}
	}
	set("SESSIONGUARD_MASTER_URL", &c.MasterURL)
	set("SESSIONGUARD_ENROLLMENT_TOKEN", &c.EnrollmentToken)
	set("SESSIONGUARD_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret)
}

func read(path string, out any) error {
	b, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return json.Unmarshal(b, out)
}
func SaveJSON(path string, v any) error {
	b, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, b, 0o600); err != nil {
		return err
	}
	return os.Rename(tmp, path)
}
func validateOIDC(c model.OIDCConfig) error {
	if c.Issuer == "" || c.ClientID == "" || c.RedirectURL == "" {
		return errors.New("oidc issuer, client_id and redirect_url are required")
	}
	for label, raw := range map[string]string{"redirect_url": c.RedirectURL, "logout_redirect_url": c.LogoutRedirectURL} {
		if strings.TrimSpace(raw) == "" {
			continue
		}
		u, err := url.Parse(strings.TrimSpace(raw))
		if err != nil || u.Hostname() == "" || u.Scheme == "" {
			return fmt.Errorf("oidc.%s must be an absolute URL", label)
		}
		if c.SecureCookie && !strings.EqualFold(u.Scheme, "https") {
			return fmt.Errorf("oidc.%s must use https when secure_cookie is enabled", label)
		}
	}
	return nil
}

func validateAccessAuth(c model.AccessAuthConfig) error {
	if strings.TrimSpace(c.Issuer) == "" || strings.TrimSpace(c.ClientID) == "" || strings.TrimSpace(c.ClientSecret) == "" || strings.TrimSpace(c.RedirectURL) == "" || strings.TrimSpace(c.LogoutRedirectURL) == "" {
		return errors.New("access_auth issuer, client_id, client_secret, redirect_url and logout_redirect_url are required when access_auth is enabled")
	}
	for label, raw := range map[string]string{"redirect_url": c.RedirectURL, "logout_redirect_url": c.LogoutRedirectURL} {
		u, err := url.Parse(strings.TrimSpace(raw))
		if err != nil || u.Hostname() == "" || u.Scheme == "" {
			return fmt.Errorf("access_auth.%s must be an absolute URL", label)
		}
		if c.SecureCookie && !strings.EqualFold(u.Scheme, "https") {
			return fmt.Errorf("access_auth.%s must use https when secure_cookie is enabled", label)
		}
	}
	if c.SessionHours < 1 || c.SessionHours > 168 {
		return errors.New("access_auth.session_hours must be between 1 and 168")
	}
	if strings.ContainsAny(c.CookieName, " ;,\t\r\n") {
		return errors.New("access_auth.cookie_name contains invalid characters")
	}
	return nil
}

func NormalizePolicy(p *model.Policy) {
	if p.Cleanup.GraceSeconds <= 0 {
		p.Cleanup.GraceSeconds = 600
	}
	if p.Cleanup.PollSeconds <= 0 {
		p.Cleanup.PollSeconds = 10
	}
	if p.Cleanup.RetrySeconds <= 0 {
		p.Cleanup.RetrySeconds = 60
	}
	if len(p.Cleanup.AllowedProfileRoots) == 0 {
		p.Cleanup.AllowedProfileRoots = []string{`C:\Users`}
	}
	if p.Profiles.BackupDelaySeconds < 0 {
		p.Profiles.BackupDelaySeconds = 0
	}
	if p.Profiles.BackupDelaySeconds == 0 {
		p.Profiles.BackupDelaySeconds = 5
	}
	if p.Profiles.RetrySeconds <= 0 {
		p.Profiles.RetrySeconds = 60
	}
	if p.Profiles.RestoreWindowSeconds <= 0 {
		p.Profiles.RestoreWindowSeconds = 120
	}
	if p.Profiles.KeepVersions < 0 {
		p.Profiles.KeepVersions = 0
	}
	if p.Sessions.DisconnectedTimeoutSeconds <= 0 {
		p.Sessions.DisconnectedTimeoutSeconds = 3600
	}
}

func ValidatePolicy(p model.Policy) error {
	if p.Cleanup.GraceSeconds < 1 || p.Cleanup.PollSeconds < 2 || p.Cleanup.RetrySeconds < 1 {
		return errors.New("invalid cleanup timing")
	}
	if p.Profiles.Enabled {
		if strings.TrimSpace(p.Profiles.StoreRoot) == "" {
			return errors.New("profiles.store_root is required when profile sync is enabled")
		}
		if len(p.Profiles.Folders) == 0 {
			return errors.New("at least one profiles.folders entry is required when profile sync is enabled")
		}
		if p.Profiles.BackupDelaySeconds < 0 {
			return errors.New("profiles.backup_delay_seconds must be >= 0")
		}
		if p.Profiles.RetrySeconds < 1 {
			return errors.New("profiles.retry_seconds must be >= 1")
		}
		if p.Profiles.RestoreWindowSeconds < 10 {
			return errors.New("profiles.restore_window_seconds must be >= 10")
		}
		for _, f := range p.Profiles.Folders {
			v := strings.ReplaceAll(strings.TrimSpace(f.Path), `\`, "/")
			if v == "" || strings.HasPrefix(v, "/") || strings.Contains(v, ":") || v == ".." || strings.HasPrefix(v, "../") || strings.Contains(v, "/../") {
				return errors.New("profile folder paths must be relative and may not escape the user profile")
			}
		}
	}
	if p.Sessions.DisconnectedLogoffEnabled && p.Sessions.DisconnectedTimeoutSeconds < 60 {
		return errors.New("sessions.disconnected_timeout_seconds must be >= 60")
	}
	return nil
}
