// Package instancecheck runs the operator-facing setup and health checks.
//
// A check answers one question an operator would otherwise have to answer by
// reading source: is this instance misconfigured, and what is the exact fix.
// Every message names the variable and the command, because a health page that
// only says "something is wrong" costs more time than it saves.
package instancecheck

import (
	"context"
	"sort"
	"sync"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/warmbly/warmbly/internal/app/instanceconfig"
	"github.com/warmbly/warmbly/internal/app/updates"
	"github.com/warmbly/warmbly/internal/config"
	"github.com/warmbly/warmbly/internal/infrastructure/cache"
	"github.com/warmbly/warmbly/internal/notify"
)

// perCheckTimeout bounds each check so one dead dependency cannot stall the
// whole page. Sized for the slowest check: two probe URLs, two attempts each.
const perCheckTimeout = 8 * time.Second

// Severity levels. Only non-ok findings are returned, so there is no "ok".
type Severity string

const (
	SeverityError   Severity = "error"
	SeverityWarning Severity = "warning"
	SeverityInfo    Severity = "info"
)

// Categories the admin panel groups findings under.
const (
	CategorySecurity = "security"
	CategoryURLs     = "urls"
	CategoryMail     = "mail"
	CategoryAccess   = "access"
	CategoryWorkers  = "workers"
	CategoryData     = "data"
	CategoryUpdates  = "updates"
)

// Finding is one thing that is wrong, plus how to fix it.
type Finding struct {
	ID       string   `json:"id"`
	Category string   `json:"category"`
	Severity Severity `json:"severity"`
	Title    string   `json:"title"`
	Message  string   `json:"message"`
	Docs     string   `json:"docs"`
	// Target names the subject of a per-record finding (a mailbox address, a
	// worker name). Empty when the finding is instance-wide.
	Target string `json:"target"`
}

// Summary counts findings by severity.
type Summary struct {
	Error   int `json:"error"`
	Warning int `json:"warning"`
	Info    int `json:"info"`
}

// Input carries the per-request facts a check cannot derive from the process.
type Input struct {
	// Host is the Host header the operator reached this backend on.
	Host string
	// Origin is the browser origin of the calling panel, when it sent one.
	Origin string
	// Forwarded reports whether the request carried X-Forwarded-For.
	Forwarded bool
}

// Deps are the process handles the checks read. Every field is optional: a
// check whose input is missing is skipped silently, never reported as failing.
type Deps struct {
	Runtime   *instanceconfig.Runtime
	Transport *notify.Transport
	Policy    *config.AuthPolicy
	DB        *pgxpool.Pool
	Cache     *cache.Cache
	// Updates is the release and updater state; nil skips the update checks.
	Updates *updates.Service
}

type check struct {
	id  string
	run func(ctx context.Context, d Deps, in Input) *Finding
}

// Registry holds the check set. Build it once at boot and reuse it.
type Registry struct {
	deps   Deps
	checks []check
}

// New builds the phase 1 registry.
func New(deps Deps) *Registry {
	r := &Registry{deps: deps}
	r.checks = append(r.checks, securityChecks()...)
	r.checks = append(r.checks, urlChecks()...)
	r.checks = append(r.checks, mailChecks()...)
	r.checks = append(r.checks, accessChecks()...)
	r.checks = append(r.checks, infraChecks()...)
	r.checks = append(r.checks, updateChecks()...)
	return r
}

// Run executes every check in parallel and returns the non-ok findings, most
// severe first, with ties broken by registration order.
func (r *Registry) Run(ctx context.Context, in Input) ([]Finding, Summary) {
	found := make([]*Finding, len(r.checks))

	var wg sync.WaitGroup
	for i, c := range r.checks {
		wg.Add(1)
		go func(i int, c check) {
			defer wg.Done()
			// A panicking check must degrade to a skip, never to a 500 on the
			// page an operator opens because something is already broken.
			defer func() { _ = recover() }()

			cctx, cancel := context.WithTimeout(ctx, perCheckTimeout)
			defer cancel()
			found[i] = c.run(cctx, r.deps, in)
		}(i, c)
	}
	wg.Wait()

	out := make([]Finding, 0, len(found))
	var summary Summary
	for i, f := range found {
		if f == nil {
			continue
		}
		f.ID = r.checks[i].id
		switch f.Severity {
		case SeverityError:
			summary.Error++
		case SeverityWarning:
			summary.Warning++
		default:
			summary.Info++
		}
		out = append(out, *f)
	}

	sort.SliceStable(out, func(i, j int) bool {
		return severityRank(out[i].Severity) > severityRank(out[j].Severity)
	})
	return out, summary
}

func severityRank(s Severity) int {
	switch s {
	case SeverityError:
		return 3
	case SeverityWarning:
		return 2
	default:
		return 1
	}
}
