// Package config defines the configuration file.
package config

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"git.sr.ht/~emersion/go-scfg"
)

// AlertMode determines if alerts grouped by Alertmanager are being kept together.
type AlertMode int

// The different modes for AlertMode.
const (
	Single AlertMode = iota
	Multi
)

// Config is the configuration of the bridge.
type Config struct {
	BaseURL     string
	HTTPAddress string
	LogLevel    string
	LogFormat   string
	LogFile     string
	AlertMode   AlertMode
	User        string
	Password    string
	Ntfy        ntfyConfig
	Labels      labels
	Cache       CacheConfig
	Am          alertmanagerConfig
	Resolved    resolvedConfig
}

type ntfyConfig struct {
	Server            string
	Topic             string
	User              string
	Password          string
	AccessToken       string
	CertFingerprint   string
	EmailAddress      string
	Call              string
	GeneratorURLLabel string
}

type labels struct {
	Order []string
	Label map[string]labelConfig
}

type labelConfig struct {
	Priority     string
	Tags         []string
	Icon         string
	EmailAddress string
	Call         string
	Topic        string
}

// CacheConfig is the configuration of the cache.
type CacheConfig struct {
	// shared settings
	Type     string
	Duration time.Duration
	// memory settings
	CleanupInterval time.Duration
	// redis settings
	RedisURL string
}

type alertmanagerConfig struct {
	User            string
	Password        string
	SilenceDuration time.Duration
	URL             string
}

type resolvedConfig struct {
	Tags     []string
	Icon     string
	Priority string
}

func parseBlock(block scfg.Block, config *Config) error {
	d := block.Get("log-level")
	if d != nil {
		if err := d.ParseParams(&config.LogLevel); err != nil {
			return err
		}
	}

	d = block.Get("log-format")
	if d != nil {
		if err := d.ParseParams(&config.LogFormat); err != nil {
			return err
		}
	}

	d = block.Get("log-file")
	if d != nil {
		if err := d.ParseParams(&config.LogFile); err != nil {
			return err
		}
	}

	d = block.Get("http-address")
	if d != nil {
		if err := d.ParseParams(&config.HTTPAddress); err != nil {
			return err
		}
	}

	d = block.Get("base-url")
	if d != nil {
		if err := d.ParseParams(&config.BaseURL); err != nil {
			return err
		}
	}

	d = block.Get("alert-mode")
	if d != nil {
		var mode string
		if err := d.ParseParams(&mode); err != nil {
			return err
		}

		switch strings.ToLower(mode) {
		case "single":
			config.AlertMode = Single

		case "multi":
			config.AlertMode = Multi

		default:
			return fmt.Errorf("%q directive: illegal mode %q", d.Name, mode)
		}
	}

	d = block.Get("user")
	if d != nil {
		if err := d.ParseParams(&config.User); err != nil {
			return err
		}
	}

	d = block.Get("password")
	if d != nil {
		if err := d.ParseParams(&config.Password); err != nil {
			return err
		}
	}

	labelsDir := block.Get("labels")
	if labelsDir != nil {
		d = labelsDir.Children.Get("order")
		if d != nil {
			var order string
			if err := d.ParseParams(&order); err != nil {
				return err
			}

			config.Labels.Order = strings.Split(order, ",")
		}

		labels := make(map[string]labelConfig)
		for _, labelName := range config.Labels.Order {
			for _, labelDir := range labelsDir.Children.GetAll(labelName) {
				labelConfig := new(labelConfig)
				var name string

				if err := labelDir.ParseParams(&name); err != nil {
					return err
				}

				d = labelDir.Children.Get("priority")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Priority); err != nil {
						return err
					}
				}

				d = labelDir.Children.Get("tags")
				if d != nil {
					var tags string
					if err := d.ParseParams(&tags); err != nil {
						return err
					}

					labelConfig.Tags = strings.Split(tags, ",")
				}

				d = labelDir.Children.Get("icon")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Icon); err != nil {
						return err
					}
				}

				d = labelDir.Children.Get("email-address")
				if d != nil {
					if err := d.ParseParams(&labelConfig.EmailAddress); err != nil {
						return err
					}
				}

				d = labelDir.Children.Get("call")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Call); err != nil {
						return err
					}
				}

				d = labelDir.Children.Get("topic")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Topic); err != nil {
						return err
					}
				}

				labels[fmt.Sprintf("%s:%s", labelName, name)] = *labelConfig
			}
		}

		config.Labels.Label = labels
	}

	ntfyDir := block.Get("ntfy")
	if ntfyDir != nil {
		d = ntfyDir.Children.Get("server")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Server); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("topic")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Topic); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("user")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.User); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("password")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Password); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("access-token")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("certificate-fingerprint")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
				return err
			}

			// hex.EncodeToString outputs a lower case string
			config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
		}

		d = ntfyDir.Children.Get("email-address")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("call")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Call); err != nil {
				return err
			}
		}

		d = ntfyDir.Children.Get("generator-url-label")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.GeneratorURLLabel); err != nil {
				return err
			}
		}
	}

	cacheDir := block.Get("cache")
	if cacheDir != nil {
		d = cacheDir.Children.Get("type")
		if d != nil {
			if err := d.ParseParams(&config.Cache.Type); err != nil {
				return err
			}
		}

		var durationString string
		d = cacheDir.Children.Get("duration")
		if d != nil {
			if err := d.ParseParams(&durationString); err != nil {
				return err
			}

			duration, err := time.ParseDuration(durationString)
			if err != nil {
				return err
			}

			config.Cache.Duration = duration
		}

		// memory
		var cleanupIntervalString string
		d = cacheDir.Children.Get("cleanup-interval")
		if d != nil {
			if err := d.ParseParams(&cleanupIntervalString); err != nil {
				return err
			}

			interval, err := time.ParseDuration(cleanupIntervalString)
			if err != nil {
				return err
			}

			config.Cache.CleanupInterval = interval
		}

		// redis
		d = cacheDir.Children.Get("redis-url")
		if d != nil {
			if err := d.ParseParams(&config.Cache.RedisURL); err != nil {
				return err
			}
		}
	}

	amDir := block.Get("alertmanager")
	if amDir != nil {
		var durationString string
		d = amDir.Children.Get("silence-duration")
		if d != nil {
			if err := d.ParseParams(&durationString); err != nil {
				return err
			}

			duration, err := time.ParseDuration(durationString)
			if err != nil {
				return err
			}

			config.Am.SilenceDuration = duration
		}

		d = amDir.Children.Get("user")
		if d != nil {
			if err := d.ParseParams(&config.Am.User); err != nil {
				return err
			}
		}

		d = amDir.Children.Get("password")
		if d != nil {
			if err := d.ParseParams(&config.Am.Password); err != nil {
				return err
			}
		}

		d = amDir.Children.Get("url")
		if d != nil {
			if err := d.ParseParams(&config.Am.URL); err != nil {
				return err
			}
		}
	}

	resolvedDir := block.Get("resolved")
	if resolvedDir != nil {
		d = resolvedDir.Children.Get("tags")
		if d != nil {
			var tags string
			if err := d.ParseParams(&tags); err != nil {
				return err
			}

			config.Resolved.Tags = strings.Split(tags, ",")
		}

		d = resolvedDir.Children.Get("icon")
		if d != nil {
			if err := d.ParseParams(&config.Resolved.Icon); err != nil {
				return err
			}
		}

		d = resolvedDir.Children.Get("priority")
		if d != nil {
			if err := d.ParseParams(&config.Resolved.Priority); err != nil {
				return err
			}
		}
	}

	return nil
}

// Read reads an scfg formatted file and returns the configuration struct.
func Read(path string) (*Config, error) {
	cfg, err := scfg.Load(path)
	if err != nil {
		return nil, err
	}

	config := new(Config)
	// Set default values
	config.HTTPAddress = "127.0.0.1:8080"
	config.LogLevel = "info"
	config.LogFormat = "text"
	config.AlertMode = Multi

	config.Ntfy.Server = "https://ntfy.sh"

	config.Cache.Type = "disabled"
	config.Cache.Duration = time.Hour * 24
	// memory
	config.Cache.CleanupInterval = time.Hour
	// redis
	config.Cache.RedisURL = "redis://localhost:6379"

	includeDirs := cfg.GetAll("include")
	for _, d := range includeDirs {
		var includePath string
		if err := d.ParseParams(&includePath); err != nil {
			return nil, err
		}

		block, err := scfg.Load(includePath)
		if err != nil {
			return nil, fmt.Errorf("cannot load included config file %q: %v", includePath, err)
		}

		if err := parseBlock(block, config); err != nil {
			return nil, fmt.Errorf("cannot parse included config file %q: %v", includePath, err)
		}
	}

	err = parseBlock(cfg, config)
	if err != nil {
		return nil, err
	}

	// Check settings
	if (config.Password != "" && config.User == "") ||
		(config.Password == "" && config.User != "") {
		return nil, errors.New("user and password have to be set together")
	}

	if config.Ntfy.Topic == "" {
		return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy")
	}

	if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
		(config.Ntfy.Password == "" && config.Ntfy.User != "") {
		return nil, errors.New("ntfy: user and password have to be set together")
	}

	if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" {
		return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
	}

	if (config.Am.Password != "" && config.Am.User == "") ||
		(config.Am.Password == "" && config.Am.User != "") {
		return nil, errors.New("alertmanager: user and password have to be set together")
	}

	return config, nil
}