package proxmox

import (
	"context"
	"fmt"
	"strings"
	"time"
)

// TaskStatus is GET /nodes/{node}/tasks/{upid}/status. While the task runs,
// Status == "running" and ExitStatus is empty; once it stops, Status == "stopped"
// and ExitStatus is "OK" or an error string (e.g. a 403 permission message).
type TaskStatus struct {
	UPID       string `json:"upid"`
	ID         string `json:"id"`
	Node       string `json:"node"`
	Type       string `json:"type"`
	User       string `json:"user"`
	Status     string `json:"status"`     // "running" | "stopped"
	ExitStatus string `json:"exitstatus"` // present once stopped
	PID        int64  `json:"pid"`
	StartTime  int64  `json:"starttime"`
}

// Running reports whether the task is still executing.
func (t TaskStatus) Running() bool { return t.Status == "running" }

// OK reports whether the task stopped successfully.
func (t TaskStatus) OK() bool { return t.Status == "stopped" && t.ExitStatus == "OK" }

// taskLogLine is one entry of GET /nodes/{node}/tasks/{upid}/log: {"n":N,"t":"..."}.
type taskLogLine struct {
	N int    `json:"n"`
	T string `json:"t"`
}

// WaitOptions tunes WaitTask polling. Zero value yields sane defaults.
type WaitOptions struct {
	// Interval is the first poll gap (default 1s).
	Interval time.Duration
	// MaxInterval caps the backed-off gap (default 5s).
	MaxInterval time.Duration
	// Timeout bounds the whole wait (default 10m). Restore/vzdump can be slow;
	// callers may raise it. A zero/elapsed context deadline also stops the wait.
	Timeout time.Duration
	// AllowWarnings accepts a task that completes with a "WARNINGS: N" exitstatus as
	// success (returning the TaskStatus with ExitStatus intact, nil error), instead of
	// the default hard failure. Opt-in PER CALL — the default (false) keeps every existing
	// caller strict, because vzdump/restore/destroy warnings can be meaningful. Only the
	// restore-test's guest-start step opts in (a start advisory like the systemd-nesting
	// notice must not false-fail a guest that actually boots; doc 03 §8). A non-WARNINGS
	// non-OK exit is still a *TaskError regardless of this flag.
	AllowWarnings bool
}

func (o WaitOptions) withDefaults() WaitOptions {
	if o.Interval <= 0 {
		o.Interval = 1 * time.Second
	}
	if o.MaxInterval <= 0 {
		o.MaxInterval = 5 * time.Second
	}
	if o.Timeout <= 0 {
		o.Timeout = 10 * time.Minute
	}
	return o
}

// TaskStatusOnce fetches the current task status (one HTTP call).
func (c *Client) TaskStatusOnce(ctx context.Context, upid string) (TaskStatus, error) {
	u, err := ParseUPID(upid)
	if err != nil {
		return TaskStatus{}, err
	}
	var st TaskStatus
	path := fmt.Sprintf("/nodes/%s/tasks/%s/status", u.Node, urlEscape(upid))
	if err := c.get(ctx, path, &st); err != nil {
		return TaskStatus{}, err
	}
	return st, nil
}

// TaskLogTail fetches up to limit trailing log lines for a task (for diagnosis).
func (c *Client) TaskLogTail(ctx context.Context, upid string, limit int) ([]string, error) {
	u, err := ParseUPID(upid)
	if err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 20
	}
	var lines []taskLogLine
	path := fmt.Sprintf("/nodes/%s/tasks/%s/log?limit=%d", u.Node, urlEscape(upid), limit)
	if err := c.get(ctx, path, &lines); err != nil {
		return nil, err
	}
	out := make([]string, 0, len(lines))
	for _, l := range lines {
		out = append(out, l.T)
	}
	return out, nil
}

// WaitTask polls a task until it stops, then asserts exitstatus == "OK". On any
// non-OK exit it returns a *TaskError carrying the exitstatus, the parsed
// privilege (if it was a permission failure), and a tail of the task log.
//
// This is the contract for EVERY mutating op: the POST's HTTP 200 is not proof of
// success — authorization can fail at task execution (phase1-2 §1.3).
func (c *Client) WaitTask(ctx context.Context, upid string, opts WaitOptions) (TaskStatus, error) {
	opts = opts.withDefaults()
	if _, err := ParseUPID(upid); err != nil {
		return TaskStatus{}, err
	}

	ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
	defer cancel()

	interval := opts.Interval
	timer := time.NewTimer(0) // first poll immediately
	defer timer.Stop()

	for {
		select {
		case <-ctx.Done():
			return TaskStatus{}, fmt.Errorf("proxmox: waiting for task %s: %w", upid, ctx.Err())
		case <-timer.C:
		}

		st, err := c.TaskStatusOnce(ctx, upid)
		if err != nil {
			return TaskStatus{}, err
		}
		if st.Running() || st.Status == "" {
			// back off, capped
			interval *= 2
			if interval > opts.MaxInterval {
				interval = opts.MaxInterval
			}
			timer.Reset(interval)
			continue
		}
		// stopped
		if st.ExitStatus == "OK" {
			return st, nil
		}
		// Warnings-accepted path (opt-in): a "WARNINGS: N" exit is returned as success with
		// ExitStatus intact, so the caller can fetch/surface the warning text from the log
		// without the test/op failing on an advisory. Any other non-OK exit still fails.
		if opts.AllowWarnings && strings.HasPrefix(st.ExitStatus, "WARNINGS") {
			return st, nil
		}
		tail, _ := c.TaskLogTail(ctx, upid, 20) // best-effort
		return st, newTaskError(upid, st.ExitStatus, tail)
	}
}
