package models

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"github.com/robfig/cron/v3"
)

// validateBackupTarget checks that a backup target is well-formed enough to
// store: a known method, and (if set) a parseable cron schedule.
func validateBackupTarget(t BackupTarget) error {
	switch t.Method {
	case MethodRsync, MethodRestic, MethodExternal:
	default:
		return fmt.Errorf("unknown backup method: %s", t.Method)
	}
	if t.Schedule != "" {
		if _, err := cron.ParseStandard(t.Schedule); err != nil {
			return fmt.Errorf("invalid schedule %q: %w", t.Schedule, err)
		}
	}
	return nil
}

// RunBackup executes backup for all targets against sourcePath and returns one
// execution record per target. scope identifies where progress is published
// ("node" or "svc:<serviceID>").
func RunBackup(scope string, targets []BackupTarget, sourcePath string) []BackupExecution {
	results := make([]BackupExecution, 0, len(targets))
	for _, t := range targets {
		results = append(results, runTarget(scope, t, sourcePath))
	}
	return results
}

func runTarget(scope string, t BackupTarget, sourcePath string) BackupExecution {
	start := time.Now()
	ex := BackupExecution{
		ID:        fmt.Sprintf("bex_%d", start.UnixNano()),
		Timestamp: start.UTC(),
		TargetID:  t.ID,
	}
	key := progressKey(scope, t.ID)
	startedAt := start.UTC()
	setProgress(key, RunState{Running: true, TargetID: t.ID, Method: t.Method, StartedAt: &startedAt})

	if t.Method == MethodExternal {
		ex.Success = true
		ex.Message = "external backup managed externally; no action taken by API"
		setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Percent: 100, Message: ex.Message, StartedAt: &startedAt})
		return ex
	}

	var cmd *exec.Cmd
	switch t.Method {
	case MethodRsync:
		args := []string{"-avz", "--info=progress2"}
		for k, v := range t.Params {
			args = append(args, "--"+k+"="+v)
		}
		args = append(args, sourcePath, t.Remote)
		cmd = exec.Command("rsync", args...)
	case MethodRestic:
		password := GetResticPassword()
		if password == "" {
			ex.Success = false
			ex.Message = "no restic repository password configured on this node; set restic_password via PUT /v1/node"
			setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
			return ex
		}
		env := append(os.Environ(), "RESTIC_REPOSITORY="+t.Remote, "RESTIC_PASSWORD="+password)
		if err := ensureResticRepo(env); err != nil {
			ex.Success = false
			ex.Message = fmt.Sprintf("restic init failed: %v", err)
			setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
			return ex
		}
		args := []string{"backup", sourcePath, "--json"}
		for k, v := range t.Params {
			args = append(args, "--"+k+"="+v)
		}
		cmd = exec.Command("restic", args...)
		cmd.Env = env
	default:
		ex.Success = false
		ex.Message = fmt.Sprintf("unknown backup method: %s", t.Method)
		setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
		return ex
	}

	pw := &progressWriter{key: key, targetID: t.ID, method: t.Method, startedAt: startedAt}
	cmd.Stdout = pw
	cmd.Stderr = pw

	err := cmd.Run()
	ex.Duration = int64(time.Since(start).Seconds())
	out := strings.TrimSpace(pw.buf.String())
	if err != nil {
		ex.Success = false
		ex.Message = fmt.Sprintf("%v | %s", err, out)
	} else {
		ex.Success = true
		if pw.lastMessage != "" {
			ex.Message = pw.lastMessage
		} else {
			ex.Message = out
		}
	}

	final := pw.state()
	final.Running = false
	if ex.Success {
		final.Percent = 100
	}
	final.Message = ex.Message
	setProgress(key, final)

	return ex
}

// ensureResticRepo initializes the restic repository named by env's
// RESTIC_REPOSITORY on first use. Running "init" against a repository that
// already exists fails with a message containing "already"; that case is
// treated as success so callers don't need to track init state themselves.
func ensureResticRepo(env []string) error {
	cmd := exec.Command("restic", "init")
	cmd.Env = env
	out, err := cmd.CombinedOutput()
	if err == nil {
		return nil
	}
	if strings.Contains(strings.ToLower(string(out)), "already") {
		return nil
	}
	return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
}

// progressWriter is assigned as both Stdout and Stderr of a backup command. It
// buffers the full output (for the final BackupExecution.Message) while also
// scanning completed lines for progress info to publish via setProgress.
// Best-effort only: a line that doesn't parse just leaves the last known
// percentage/ETA in place rather than failing the backup.
type progressWriter struct {
	buf         bytes.Buffer
	pending     []byte
	key         string
	targetID    string
	method      BackupMethod
	startedAt   time.Time
	percent     float64
	eta         string
	lastMessage string
}

func (w *progressWriter) Write(p []byte) (int, error) {
	w.buf.Write(p)
	w.pending = append(w.pending, p...)
	for {
		idx := bytes.IndexAny(w.pending, "\r\n")
		if idx < 0 {
			break
		}
		line := bytes.TrimSpace(w.pending[:idx])
		w.pending = w.pending[idx+1:]
		if len(line) > 0 {
			w.handleLine(line)
		}
	}
	return len(p), nil
}

func (w *progressWriter) handleLine(line []byte) {
	switch w.method {
	case MethodRsync:
		w.handleRsyncLine(line)
	case MethodRestic:
		w.handleResticLine(line)
	}
	setProgress(w.key, w.state())
}

var rsyncPercentRe = regexp.MustCompile(`(\d+)%`)

func (w *progressWriter) handleRsyncLine(line []byte) {
	w.lastMessage = string(line)
	m := rsyncPercentRe.FindSubmatch(line)
	if m == nil {
		return
	}
	var pct float64
	if _, err := fmt.Sscanf(string(m[1]), "%f", &pct); err == nil {
		w.percent = pct
	}
}

// resticStatusLine models the subset of restic's `backup --json` line kinds
// (message_type "status", "summary", "error") that are useful for progress
// reporting; unrecognized/unparseable lines are ignored.
type resticStatusLine struct {
	MessageType      string  `json:"message_type"`
	PercentDone      float64 `json:"percent_done"`
	SecondsRemaining float64 `json:"seconds_remaining"`
	SnapshotID       string  `json:"snapshot_id"`
	Error            *struct {
		Message string `json:"message"`
	} `json:"error,omitempty"`
}

func (w *progressWriter) handleResticLine(line []byte) {
	var sl resticStatusLine
	if err := json.Unmarshal(line, &sl); err != nil {
		return
	}
	switch sl.MessageType {
	case "status":
		w.percent = sl.PercentDone * 100
		if sl.SecondsRemaining > 0 {
			w.eta = fmt.Sprintf("%.0fs", sl.SecondsRemaining)
		}
	case "summary":
		w.percent = 100
		if sl.SnapshotID != "" {
			w.lastMessage = fmt.Sprintf("snapshot %s created", sl.SnapshotID)
		}
	case "error":
		if sl.Error != nil && sl.Error.Message != "" {
			w.lastMessage = sl.Error.Message
		}
	}
}

func (w *progressWriter) state() RunState {
	startedAt := w.startedAt
	return RunState{
		Running:   true,
		TargetID:  w.targetID,
		Method:    w.method,
		Percent:   w.percent,
		ETA:       w.eta,
		Message:   w.lastMessage,
		StartedAt: &startedAt,
	}
}

// StartService runs the compose start command for the given service.
func StartService(svc Service) error {
	cmd := composeCmd(svc, "start")
	if out, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
	}
	return nil
}

// StopService runs the compose stop command for the given service.
func StopService(svc Service) error {
	cmd := composeCmd(svc, "stop")
	if out, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
	}
	return nil
}

func composeCmd(svc Service, action string) *exec.Cmd {
	switch svc.ComposeType {
	case "docker-compose":
		return exec.Command("docker-compose", "-f", svc.ComposeFile, action)
	case "podman-compose":
		return exec.Command("podman-compose", "-f", svc.ComposeFile, action)
	default:
		return exec.Command("docker", "compose", "-f", svc.ComposeFile, action)
	}
}

// filterTargets returns the subset of targets whose ID is in ids. A nil or
// empty ids means "all targets".
func filterTargets(targets []BackupTarget, ids []string) []BackupTarget {
	if len(ids) == 0 {
		return targets
	}
	want := make(map[string]bool, len(ids))
	for _, id := range ids {
		want[id] = true
	}
	result := make([]BackupTarget, 0, len(ids))
	for _, t := range targets {
		if want[t.ID] {
			result = append(result, t)
		}
	}
	return result
}
