package main

import (
	"bufio"
	"bytes"
	_ "embed"
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/fsnotify/fsnotify"
)

//go:embed templates/config.toml
var defaultConfigTemplate string

//go:embed templates/default.env
var defaultEnvTemplate string

type Schedule struct {
	Cron      string `toml:"cron"`
	OnBattery bool   `toml:"on_battery"`
	Backup    *bool  `toml:"backup"`
	Prune     bool   `toml:"prune"`
	Check     bool   `toml:"check"`
}

func (s Schedule) BackupEnabled() bool {
	return s.Backup == nil || *s.Backup
}

type Backup struct {
	Paths         []string `toml:"paths"`
	Args          []string `toml:"args"`
	ArgsScheduled []string `toml:"args_scheduled"`
}

type OperationOptions struct {
	Args []string `toml:"args"`
}

type Profile struct {
	Name             string           `toml:"name"`
	Backend          string           `toml:"backend"`
	EnvFile          string           `toml:"env_file"`
	RcloneConfigFile string           `toml:"rclone_config_file"`
	RetryLock        string           `toml:"retry_lock"`
	PreHook          string           `toml:"pre_hook"`
	PostHook         string           `toml:"post_hook"`
	Schedule         Schedule         `toml:"schedule"`
	Backup           Backup           `toml:"backup"`
	Prune            OperationOptions `toml:"prune"`
	Check            OperationOptions `toml:"check"`
	Mount            OperationOptions `toml:"mount"`
}

func (prof Profile) profileKey() stateKey {
	return stateKey(prof.displayName())
}

type GUI struct {
	CheckUpdates    *bool  `toml:"check_updates"`
	ManageBackend   *bool  `toml:"manage_backend"`
	ManageRestic    bool   `toml:"manage_restic"`
	Notifications   string `toml:"notifications"`
	Icon            string `toml:"icon"`
	ScheduleDisplay string `toml:"schedule_display"`
	Terminal        string `toml:"terminal"`
}

func (g GUI) UpdatesEnabled() bool {
	return g.CheckUpdates == nil || *g.CheckUpdates
}

func (g GUI) BackendManagementEnabled() bool {
	if g.ManageBackend != nil {
		return *g.ManageBackend
	}
	return g.ManageRestic
}

type Config struct {
	General  GUI       `toml:"general,omitempty"` // deprecated, use [gui]
	GUI      GUI       `toml:"gui"`
	Profile  []Profile `toml:"profile,omitempty"` // deprecated, use [[profiles]]
	Profiles []Profile `toml:"profiles"`
	loadErr  error
}

func parseEnvFile(path string) (map[string]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	env := make(map[string]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		line = strings.TrimPrefix(line, "export ")
		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		val = strings.TrimSpace(val)
		if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0] {
			val = val[1 : len(val)-1]
		}
		env[key] = val
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return env, nil
}

func (prof Profile) baseConfigError() string {
	if prof.backend() == "" {
		return "Unsupported backend: " + prof.Backend
	}
	return ""
}

func (prof Profile) repositoryError() string {
	if err := prof.baseConfigError(); err != "" {
		return err
	}
	vars, err := parseEnvFile(prof.EnvFile)
	if err != nil {
		return "Env file not available"
	}
	return backendEnvError(prof.backend(), vars)
}

func (prof Profile) scheduleError() string {
	if err := prof.scheduleDefinitionError(); err != "" {
		return err
	}
	return prof.repositoryError()
}
func (prof Profile) scheduleDefinitionError() string {
	if err := prof.baseConfigError(); err != "" {
		return err
	}
	if prof.Schedule.BackupEnabled() && len(prof.Backup.Paths) == 0 {
		return "No paths configured"
	}
	return ""
}

func (prof Profile) displayName() string {
	if prof.Name != "" {
		return prof.Name
	}
	if prof.EnvFile != "" {
		return strings.TrimSuffix(filepath.Base(prof.EnvFile), ".env")
	}
	return "Profile"
}

func envFilesInsecure(profiles []Profile) bool {
	if runtime.GOOS == "windows" {
		return false
	}
	for _, p := range profiles {
		info, err := os.Stat(p.EnvFile)
		if err == nil && info.Mode().Perm()&0077 != 0 {
			return true
		}
	}
	return false
}

func fixEnvFilePermissions(profiles []Profile) {
	for _, p := range profiles {
		os.Chmod(p.EnvFile, 0600)
	}
}

var configDirOverride string
var dataDirOverride string

func configLoadErrorMessage(err error) string {
	var parseErr toml.ParseError
	if errors.As(err, &parseErr) {
		return fmt.Sprintf("Config error: line %d, column %d", parseErr.Position.Line, parseErr.Position.Col)
	}
	return "Config error: " + err.Error()
}

func configDir() string {
	if configDirOverride != "" {
		return configDirOverride
	}
	if env := os.Getenv("RESTRAY_CONFIG"); env != "" {
		return env
	}
	dir, _ := os.UserConfigDir()
	return filepath.Join(dir, "restray")
}

func configPath() string { return filepath.Join(configDir(), "config.toml") }
func defaultEnvPath(name string) string {
	if name == "" {
		name = "default"
	}
	return filepath.Join(configDir(), name+".env")
}

func dataDir() string {
	if dataDirOverride != "" {
		return dataDirOverride
	}
	if env := os.Getenv("RESTRAY_STATE"); env != "" {
		return env
	}
	switch runtime.GOOS {
	case "darwin":
		if home, err := os.UserHomeDir(); err == nil {
			return filepath.Join(home, "Library", "Application Support", "restray")
		}
	case "windows":
		if dir := os.Getenv("LOCALAPPDATA"); dir != "" {
			return filepath.Join(dir, "Restray")
		}
	default:
		if dir := os.Getenv("XDG_STATE_HOME"); dir != "" {
			return filepath.Join(dir, "restray")
		}
		if home, err := os.UserHomeDir(); err == nil {
			return filepath.Join(home, ".local", "state", "restray")
		}
	}
	return configDir()
}

func logPath() string  { return filepath.Join(dataDir(), "restray.log") }
func lockPath() string { return filepath.Join(dataDir(), "restray.lock") }

func loadConfig() Config {
	cfg := Config{}
	if _, err := toml.DecodeFile(configPath(), &cfg); err != nil {
		if !os.IsNotExist(err) {
			cfg.loadErr = err
			log.Printf("config: %s", configLoadErrorMessage(err))
		}
	}
	if len(cfg.Profile) > 0 {
		log.Printf("config: [[profile]] is deprecated, use [[profiles]] instead")
		cfg.Profiles = append(cfg.Profiles, cfg.Profile...)
		cfg.Profile = nil
	}
	if cfg.General != (GUI{}) {
		log.Printf("config: [general] is deprecated, use [gui] instead")
		if cfg.GUI == (GUI{}) {
			cfg.GUI = cfg.General
		}
		cfg.General = GUI{}
	}
	if cfg.GUI.ScheduleDisplay == "" {
		cfg.GUI.ScheduleDisplay = "description"
	}
	if len(cfg.Profiles) == 0 {
		cfg.Profiles = append(cfg.Profiles, Profile{Name: "default"})
	}
	for i := range cfg.Profiles {
		prof := &cfg.Profiles[i]
		if prof.Backend == "" {
			prof.Backend = string(BackendRestic)
		}
		if prof.EnvFile == "" {
			prof.EnvFile = defaultEnvPath(prof.Name)
		}
		if prof.RetryLock == "" {
			prof.RetryLock = "2m"
		}
		if prof.Backup.Args == nil {
			if prof.backend() == BackendRustic {
				prof.Backup.Args = []string{}
			} else {
				prof.Backup.Args = []string{"--exclude-caches", "--exclude", "*.tmp"}
			}
		}
		if prof.Backup.ArgsScheduled == nil {
			prof.Backup.ArgsScheduled = []string{"--tag", "scheduled"}
		}
		if prof.Prune.Args == nil {
			prof.Prune.Args = []string{
				"--keep-last", "4",
				"--keep-hourly", "24",
				"--keep-daily", "7",
				"--keep-weekly", "4",
				"--keep-monthly", "12",
			}
		}
	}
	if cfg.loadErr == nil {
		names := make(map[string]struct{}, len(cfg.Profiles))
		for _, prof := range cfg.Profiles {
			name := prof.displayName()
			if _, exists := names[name]; exists {
				cfg.loadErr = fmt.Errorf("duplicate profile name %q", name)
				log.Printf("config: %s", configLoadErrorMessage(cfg.loadErr))
				break
			}
			names[name] = struct{}{}
		}
	}
	return cfg
}

func defaultConfigContents() string {
	return strings.ReplaceAll(defaultConfigTemplate, "{{ENV_FILE}}", filepath.ToSlash(defaultEnvPath("default")))
}

func ensureConfigFile() error {
	p := configPath()
	if _, err := os.Stat(p); err == nil {
		return nil
	} else if !os.IsNotExist(err) {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
		return err
	}
	return os.WriteFile(p, []byte(defaultConfigContents()), 0600)
}

func configWritable() bool {
	f, err := os.OpenFile(configPath(), os.O_WRONLY, 0)
	if err != nil {
		return false
	}
	f.Close()
	return true
}

func saveConfig(cfg Config) error {
	var buf bytes.Buffer
	enc := toml.NewEncoder(&buf)
	if err := enc.Encode(cfg); err != nil {
		return err
	}
	p := configPath()
	os.MkdirAll(filepath.Dir(p), 0700)
	return os.WriteFile(p, buf.Bytes(), 0600)
}

func ensureEnvFile(path string) error {
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !os.IsNotExist(err) {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return err
	}
	return os.WriteFile(path, []byte(defaultEnvTemplate), 0600)
}

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

func watchConfig(onChange func()) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Printf("config: failed to watch config directory: %v", err)
		return
	}
	if err := watcher.Add(configDir()); err != nil {
		log.Printf("config: failed to watch %s: %v", configDir(), err)
		watcher.Close()
		return
	}

	go func() {
		for err := range watcher.Errors {
			log.Printf("config watch: %v", err)
		}
	}()
	go func() {
		var debounce *time.Timer
		for event := range watcher.Events {
			ext := filepath.Ext(event.Name)
			if ext != ".toml" && ext != ".env" {
				continue
			}
			action := event.Has(fsnotify.Write) || event.Has(fsnotify.Create) ||
				event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename)
			if action {
				if debounce != nil {
					debounce.Stop()
				}
				debounce = time.AfterFunc(200*time.Millisecond, onChange)
			}
		}
	}()
}
