package config

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gopkg.in/yaml.v2"
)

var (
	ErrConfigNotFound = errors.New("no config file found")
	ErrUnsetEnv       = errors.New("environment variable is not set")
	ErrFileNotFound   = errors.New("provided file path is invalid")
)

type ServerConfig struct {
	Host string `yaml:"host"`
	Port int    `yaml:"port"`
}

type MetaConfig struct {
	Title       string `yaml:"title"`
	Description string `yaml:"description"`
	Host        string `yaml:"host"`
	Modt        string `yaml:"modt"`
}

type RepoConfig struct {
	Dir     string   `yaml:"dir"`
	Readmes []string `yaml:"readmes"`
}

type SSHConfig struct {
	Enable  bool     `yaml:"enable"`
	User    string   `yaml:"user"`
	Keys    []string `yaml:"keys"`
	LogFile string   `yaml:"log_file"`
}

type MirrorConfig struct {
	Enable      bool          `yaml:"enable"`
	Interval    time.Duration `yaml:"interval"`
	GithubToken string        `yaml:"github_token"`
}

type CacheConfig struct {
	HomePage time.Duration `yaml:"home_page"`
	Readme   time.Duration `yaml:"readme"`
	Diff     time.Duration `yaml:"diff"`
}

type Config struct {
	Server ServerConfig `yaml:"server"`
	Meta   MetaConfig   `yaml:"meta"`
	Repo   RepoConfig   `yaml:"repo"`
	SSH    SSHConfig    `yaml:"ssh"`
	Mirror MirrorConfig `yaml:"mirror"`
	Cache  CacheConfig  `yaml:"cache"`
}

func Load(fpath string) (*Config, error) {
	configBytes, err := os.ReadFile(fpath)
	if err != nil {
		return nil, err
	}

	var config Config
	if cerr := yaml.Unmarshal(configBytes, &config); cerr != nil {
		return nil, fmt.Errorf("parsing config: %w", cerr)
	}

	if config.Repo.Dir, err = filepath.Abs(config.Repo.Dir); err != nil {
		return nil, err
	}

	config.ensureDefaults()

	if perr := config.parseValues(); perr != nil {
		return nil, perr
	}

	if verr := config.validate(); verr != nil {
		return nil, verr
	}

	return &config, nil
}

// PathOrDefault uses userPath, if it's "", or invalid path, will default to one of those(in priority order)
// 1. ./config.yaml
// 2. /etc/mugit.yaml
// 3. /var/lib/mugit/config.yaml
func PathOrDefault(userPath string) string {
	return pathOrDefaultWithCandidates(userPath, []string{
		"./config.yaml",
		"/etc/mugit.yaml",
		"/var/lib/mugit/config.yaml",
	})
}

func pathOrDefaultWithCandidates(path string, candidates []string) string {
	if isFileExists(path) {
		return path
	}

	for _, fpath := range candidates {
		if isFileExists(fpath) {
			return fpath
		}
	}

	return ""
}

func (c *Config) ensureDefaults() {
	// http
	if c.Server.Port == 0 {
		c.Server.Port = 8080
	}

	// meta
	if c.Meta.Title == "" {
		c.Meta.Title = "my mugit"
	}

	// repos
	if len(c.Repo.Readmes) == 0 {
		c.Repo.Readmes = []string{
			"README.md", "readme.md",
			"README.html", "readme.html",
			"README.txt", "readme.txt",
			"readme",
		}
	}

	// ssh
	if c.SSH.User == "" {
		c.SSH.User = "git"
	}
	if c.SSH.LogFile == "" {
		c.SSH.LogFile = filepath.Join(c.Repo.Dir, "mugit-ssh.log")
	}

	// mirroring
	if c.Mirror.Interval == 0 {
		c.Mirror.Interval = 8 * time.Hour
	}

	// cache
	if c.Cache.HomePage == 0 {
		c.Cache.HomePage = 5 * time.Minute
	}

	if c.Cache.Readme == 0 {
		c.Cache.Readme = 1 * time.Minute
	}

	if c.Cache.Diff == 0 {
		c.Cache.Diff = 15 * time.Minute
	}
}

func (c *Config) parseValues() error {
	if c.Mirror.Enable {
		ghToken, err := parseValue(c.Mirror.GithubToken)
		if err != nil {
			return err
		}
		c.Mirror.GithubToken = ghToken
	}
	return nil
}

func parseValue(value string) (string, error) {
	envPrefix := "$env:"
	filePrefix := "$file:"

	switch {
	case strings.HasPrefix(value, envPrefix):
		env := os.Getenv(os.ExpandEnv(value[len(envPrefix):]))
		if env == "" {
			return "", ErrUnsetEnv
		}
		return env, nil

	case strings.HasPrefix(value, filePrefix):
		// supports only absolute paths

		fpath := value[len(filePrefix):]
		if !isFileExists(fpath) {
			return "", ErrFileNotFound
		}

		data, err := os.ReadFile(fpath)
		if err != nil {
			return "", err
		}

		return strings.TrimSpace(string(data)), nil

	default:
		return value, nil
	}
}

func isFileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func isDirExists(path string) bool {
	i, err := os.Stat(path)
	if err != nil {
		return false
	}
	return i.IsDir()
}
