package main

import (
	"context"
	"encoding/json"
	"log"
	"os"
	"os/exec"
	"strings"
	"time"
)

func profileEnv(prof Profile) []string {
	env := os.Environ()
	if vars, err := parseEnvFile(prof.EnvFile); err == nil {
		for k, v := range vars {
			env = setEnv(env, k, v)
		}
	}
	if prof.RcloneConfigFile != "" {
		env = setEnv(env, "RCLONE_CONFIG", prof.RcloneConfigFile)
	}
	return env
}

func setEnv(env []string, key, value string) []string {
	for i, entry := range env {
		if k, _, ok := strings.Cut(entry, "="); ok && k == key {
			env[i] = key + "=" + value
			return env
		}
	}
	return append(env, key+"="+value)
}

type repoResult struct {
	errMsg    string
	needsInit bool
}

func probeRepo(prof Profile, args []string) ([]byte, repoResult) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	args = backendCommandArgs(prof, args...)
	path, _ := findBackend(prof)
	log.Printf("[%s] probe (%s): %s %s", prof.displayName(), prof.backendName(), prof.backendName(), strings.Join(args, " "))
	start := time.Now()
	cmd := exec.CommandContext(ctx, path, args...)
	cmd.Env = profileEnv(prof)
	hideWindow(cmd)
	var stderr strings.Builder
	cmd.Stderr = &stderr
	out, err := cmd.Output()
	if err != nil {
		trimmed := strings.TrimSpace(stderr.String())
		if ctx.Err() != nil {
			log.Printf("[%s] repo status timed out after 30s; last output: %s", prof.displayName(), trimmed)
			return nil, repoResult{errMsg: "Repository unreachable"}
		}
		log.Printf("[%s] repo status error: %s", prof.displayName(), trimmed)
		lines := strings.Split(trimmed, "\n")
		msg, code := classifyBackendError(prof, err, lines[len(lines)-1])
		needsInit := repositoryNeedsInit(prof, code, trimmed)
		if needsInit {
			msg = "Repository not initialized"
		}
		return nil, repoResult{errMsg: msg, needsInit: needsInit}
	}
	log.Printf("[%s] repo status ok (%s)", prof.displayName(), time.Since(start).Round(time.Millisecond))
	return out, repoResult{}
}

func repoStatus(prof Profile) repoResult {
	args := []string{"cat", "config"}
	if prof.backend() == BackendRestic {
		args = append(args, "--no-lock", "-q")
	}
	_, rr := probeRepo(prof, args)
	return rr
}

func repoStatusAndLastBackup(prof Profile) (repoResult, time.Time) {
	host, err := os.Hostname()
	if err != nil {
		return repoStatus(prof), time.Time{}
	}
	args := []string{"snapshots", "--json"}
	if prof.backend() == BackendRestic {
		args = append(args, "--latest", "1", "--host", host, "--no-lock")
	} else {
		args = append(args, "--filter-host", host, "--filter-last", "1", "--group-by", "host")
	}
	out, rr := probeRepo(prof, args)
	if rr.errMsg != "" {
		return rr, time.Time{}
	}
	if prof.backend() == BackendRustic {
		var groups []struct {
			Snapshots []struct {
				Time time.Time `json:"time"`
			} `json:"snapshots"`
		}
		if json.Unmarshal(out, &groups) != nil || len(groups) == 0 || len(groups[0].Snapshots) == 0 {
			return rr, time.Time{}
		}
		return rr, groups[0].Snapshots[0].Time
	}
	var snaps []struct {
		Time time.Time `json:"time"`
	}
	if json.Unmarshal(out, &snaps) != nil || len(snaps) == 0 {
		return rr, time.Time{}
	}
	return rr, snaps[0].Time
}

func exitCode(err error) int {
	if exitErr, ok := err.(*exec.ExitError); ok {
		return exitErr.ExitCode()
	}
	return -1
}

func killedBySystem(err error) bool {
	return err != nil && exitCode(err) == -1 && strings.Contains(err.Error(), "killed")
}

func classifyResticError(err error, lastStderr string) (string, int) {
	code := exitCode(err)
	if killedBySystem(err) {
		return "Process terminated by system", code
	}
	if code == 12 {
		return "Wrong password", code
	}
	if lastStderr != "" {
		var msg struct {
			Message string `json:"message"`
		}
		if json.Unmarshal([]byte(lastStderr), &msg) == nil && msg.Message != "" {
			return msg.Message, code
		}
		return lastStderr, code
	}
	if err != nil {
		return err.Error(), code
	}
	return "", code
}

func rusticNeedsInit(output string) bool {
	s := strings.ToLower(output)
	return strings.Contains(s, "repository does not exist") || strings.Contains(s, "repository is not initialized") || strings.Contains(s, "no repository config file found") || strings.Contains(s, "no such file or directory") && strings.Contains(s, "config")
}

func repositoryNeedsInit(prof Profile, code int, output string) bool {
	switch prof.backend() {
	case BackendRestic:
		return code == 10
	case BackendRustic:
		return rusticNeedsInit(output)
	default:
		return false
	}
}

func classifyBackendError(prof Profile, err error, lastStderr string) (string, int) {
	if prof.backend() == BackendRestic {
		return classifyResticError(err, lastStderr)
	}
	if killedBySystem(err) {
		return "Process terminated by system", exitCode(err)
	}
	msg := strings.TrimSpace(lastStderr)
	lower := strings.ToLower(msg)
	if strings.Contains(lower, "wrong password") || strings.Contains(lower, "password") && strings.Contains(lower, "failed") {
		return "Wrong password or key", exitCode(err)
	}
	if msg != "" {
		return msg, exitCode(err)
	}
	if err != nil {
		return err.Error(), exitCode(err)
	}
	return "", exitCode(err)
}
