package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

const appDir = "aegis"

type Settings struct {
	Host           string   `json:"host"`
	Port           string   `json:"port"`
	UseTLS         bool     `json:"use_tls"`
	StartTLS       bool     `json:"start_tls,omitempty"`
	SkipTLSVerify  bool     `json:"skip_tls_verify,omitempty"`
	Username       string   `json:"username,omitempty"`
	SASLMechanism  string   `json:"sasl_mechanism,omitempty"`
	UseCompression bool     `json:"use_compression,omitempty"`
	SMTPHost       string   `json:"smtp_host,omitempty"`
	SMTPPort       string   `json:"smtp_port,omitempty"`
	SMTPMode       string   `json:"smtp_mode,omitempty"`
	SMTPUsername   string   `json:"smtp_username,omitempty"`
	SMTPEmail      string   `json:"smtp_email,omitempty"`
	SMTPRecipient  string   `json:"smtp_recipient,omitempty"`
	SMTPSkipVerify bool     `json:"smtp_skip_tls_verify,omitempty"`
	ProxyType      string   `json:"proxy_type"`
	ProxyAddress   string   `json:"proxy_address,omitempty"`
	DisplayName    string   `json:"display_name,omitempty"`
	Email          string   `json:"email,omitempty"`
	Subscriptions  []string `json:"subscriptions,omitempty"`
}

func Default() Settings {
	return Settings{
		Host:          "news.tcpreset.net",
		Port:          "563",
		UseTLS:        true,
		SMTPHost:      "qee4i7sags6phsvb2yodwecfj7noimfhhalsjktsvikrwotxzis3raad.onion",
		SMTPPort:      "25",
		SMTPRecipient: "mail2news@mail2news.tcpreset.net",
		ProxyType:     "SOCKS5",
		ProxyAddress:  "127.0.0.1:9050",
	}
}

func DefaultPath() (string, error) {
	dir, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("locate user configuration directory: %w", err)
	}
	return filepath.Join(dir, appDir, "config.json"), nil
}

func Load(path string) (Settings, error) {
	settings := Default()
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return settings, nil
	}
	if err != nil {
		return Settings{}, fmt.Errorf("read settings: %w", err)
	}
	if err := json.Unmarshal(data, &settings); err != nil {
		return Settings{}, fmt.Errorf("decode settings: %w", err)
	}
	settings.normalize()
	if err := settings.Validate(); err != nil {
		return Settings{}, fmt.Errorf("validate saved settings: %w", err)
	}
	return settings, nil
}

func Save(path string, settings Settings) error {
	settings.normalize()
	if err := settings.Validate(); err != nil {
		return err
	}
	data, err := json.MarshalIndent(settings, "", "  ")
	if err != nil {
		return fmt.Errorf("encode settings: %w", err)
	}
	data = append(data, '\n')
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("create settings directory: %w", err)
	}
	if err := os.Chmod(dir, 0o700); err != nil {
		return fmt.Errorf("protect settings directory: %w", err)
	}
	tmp, err := os.CreateTemp(dir, ".config-*.tmp")
	if err != nil {
		return fmt.Errorf("create temporary settings file: %w", err)
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if err := tmp.Chmod(0o600); err != nil {
		tmp.Close()
		return fmt.Errorf("protect temporary settings file: %w", err)
	}
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return fmt.Errorf("write settings: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return fmt.Errorf("sync settings: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close settings: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		return fmt.Errorf("commit settings: %w", err)
	}
	return nil
}

func (s Settings) Validate() error {
	if err := validateToken("host", s.Host); err != nil {
		return err
	}
	if net.ParseIP(s.Host) == nil && strings.ContainsAny(s.Host, " /\\") {
		return errors.New("host contains invalid characters")
	}
	port, err := strconv.Atoi(s.Port)
	if err != nil || port < 1 || port > 65535 {
		return errors.New("port must be between 1 and 65535")
	}
	if err := validateToken("username", s.Username); err != nil {
		return err
	}
	if s.Username != "" && !s.UseTLS {
		return errors.New("authentication requires TLS")
	}
	if s.StartTLS && !s.UseTLS {
		return errors.New("STARTTLS requires TLS")
	}
	if s.SASLMechanism != "" && !strings.EqualFold(s.SASLMechanism, "PLAIN") {
		return errors.New("unsupported SASL mechanism")
	}
	if s.SASLMechanism != "" && s.Username == "" {
		return errors.New("SASL requires a username")
	}
	if s.UseCompression && s.UseTLS {
		return errors.New("COMPRESS DEFLATE cannot be used with TLS")
	}
	if err := validateSMTP(s); err != nil {
		return err
	}
	switch s.ProxyType {
	case "DIRECT":
	case "SOCKS5":
		if _, _, err := net.SplitHostPort(s.ProxyAddress); err != nil {
			return fmt.Errorf("invalid SOCKS5 address: %w", err)
		}
	default:
		return errors.New("proxy type must be DIRECT or SOCKS5")
	}
	if strings.ContainsAny(s.DisplayName, "\r\n") || strings.ContainsAny(s.Email+s.SMTPEmail+s.SMTPRecipient, "\r\n") {
		return errors.New("posting identity must not contain line breaks")
	}
	for _, group := range s.Subscriptions {
		if err := ValidateGroupName(group); err != nil {
			return fmt.Errorf("invalid subscription %q: %w", group, err)
		}
	}
	return nil
}

func ValidateGroupName(group string) error {
	if err := validateToken("newsgroup", group); err != nil {
		return err
	}
	if strings.ContainsAny(group, " ,") {
		return errors.New("newsgroup contains invalid characters")
	}
	return nil
}

func (s *Settings) normalize() {
	s.Host = strings.TrimSpace(s.Host)
	s.Port = strings.TrimSpace(s.Port)
	s.Username = strings.TrimSpace(s.Username)
	s.SASLMechanism = strings.ToUpper(strings.TrimSpace(s.SASLMechanism))
	s.SMTPHost = strings.TrimSpace(s.SMTPHost)
	s.SMTPPort = strings.TrimSpace(s.SMTPPort)
	s.SMTPMode = strings.ToUpper(strings.TrimSpace(s.SMTPMode))
	if s.SMTPMode == "DISABLED" || s.SMTPMode == "CLEARTEXT" {
		s.SMTPMode = ""
	}
	s.SMTPUsername = strings.TrimSpace(s.SMTPUsername)
	s.SMTPEmail = strings.TrimSpace(s.SMTPEmail)
	s.SMTPRecipient = strings.TrimSpace(s.SMTPRecipient)
	s.ProxyType = strings.ToUpper(strings.TrimSpace(s.ProxyType))
	s.ProxyAddress = strings.TrimSpace(s.ProxyAddress)
	s.DisplayName = strings.TrimSpace(s.DisplayName)
	s.Email = strings.TrimSpace(s.Email)
	seen := make(map[string]struct{}, len(s.Subscriptions))
	groups := s.Subscriptions[:0]
	for _, group := range s.Subscriptions {
		group = strings.TrimSpace(group)
		if group == "" {
			continue
		}
		if _, ok := seen[group]; ok {
			continue
		}
		seen[group] = struct{}{}
		groups = append(groups, group)
	}
	sort.Strings(groups)
	s.Subscriptions = groups
}

func validateSMTP(s Settings) error {
	if s.SMTPHost == "" && s.SMTPMode == "" && s.SMTPPort == "" {
		return nil
	}
	if s.SMTPHost == "" || s.SMTPPort == "" {
		return errors.New("SMTP host and port are required when SMTP is configured")
	}
	if err := validateToken("SMTP host", s.SMTPHost); err != nil {
		return err
	}
	if net.ParseIP(s.SMTPHost) == nil && strings.ContainsAny(s.SMTPHost, " /\\") {
		return errors.New("SMTP host contains invalid characters")
	}
	port, err := strconv.Atoi(s.SMTPPort)
	if err != nil || port < 1 || port > 65535 {
		return errors.New("SMTP port must be between 1 and 65535")
	}
	switch s.SMTPMode {
	case "", "TLS", "STARTTLS":
	default:
		return errors.New("SMTP mode must be cleartext, TLS or STARTTLS")
	}
	if s.SMTPUsername != "" && s.SMTPMode == "" {
		return errors.New("SMTP authentication requires TLS or STARTTLS")
	}
	if strings.HasSuffix(strings.ToLower(s.SMTPHost), ".onion") && s.ProxyType != "SOCKS5" {
		return errors.New(".onion SMTP servers require a SOCKS5 proxy")
	}
	return nil
}

func validateToken(name, value string) error {
	if name == "host" && value == "" {
		return errors.New("host is required")
	}
	if strings.ContainsAny(value, "\x00\r\n\t") {
		return fmt.Errorf("%s contains control characters", name)
	}
	return nil
}
