package services

import (
	"context"
	"encoding/json"
	"regexp"
	"strings"

	"nadir/internal/oscmd"

	"github.com/danielgtaylor/huma/v2"
)

// selfUnit is nadir's own systemd unit name. Acting on it via the normal
// synchronous path would have systemd SIGTERM the very process serving the
// request, so the client sees a dropped connection / 500 even though the
// action succeeded. We detach those calls into a Setsid subprocess instead.
const selfUnit = "nadir"

const tagServices = "Services"

var (
	readErrors  = []int{401, 403, 500}
	writeErrors = []int{400, 401, 403, 404, 500}
)

// unitNameRe matches valid systemd unit names. Combined with a leading-dash
// reject and the "--" separator on every systemctl call, it keeps user-supplied
// unit names from being interpreted as options.
var unitNameRe = regexp.MustCompile(`^[a-zA-Z0-9@._:-]+$`)

// ServiceUnit mirrors one entry of `systemctl list-units --type=service -o json`.
type ServiceUnit struct {
	Unit        string `json:"unit" example:"sshd.service" doc:"Unit name"`
	Load        string `json:"load" example:"loaded" doc:"Load state"`
	Active      string `json:"active" example:"active" doc:"High-level active state"`
	Sub         string `json:"sub" example:"running" doc:"Low-level sub state"`
	Description string `json:"description" example:"OpenSSH server daemon" doc:"Unit description"`
}

type ListServicesOutput struct {
	Body struct {
		Services []ServiceUnit `json:"services" doc:"All service units, active and inactive"`
	}
}

// ServiceStatusBody is the detailed status of a single unit from `systemctl show`.
type ServiceStatusBody struct {
	Unit          string `json:"unit" example:"sshd.service"`
	Description   string `json:"description" example:"OpenSSH server daemon"`
	LoadState     string `json:"load_state" example:"loaded" doc:"loaded / not-found / masked"`
	ActiveState   string `json:"active_state" example:"active" doc:"active / inactive / failed"`
	SubState      string `json:"sub_state" example:"running"`
	UnitFileState string `json:"unit_file_state" example:"enabled" doc:"enabled / disabled / static"`
}

type GetServiceOutput struct{ Body ServiceStatusBody }

// UnitPath is the shared path parameter for per-unit operations.
type UnitPath struct {
	Unit string `path:"unit" example:"sshd.service" doc:"systemd unit name"`
}

func registerServices(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "services-list",
		Method:      "GET",
		Path:        "/api/services",
		Summary:     "List service units",
		Description: "Returns all service units (active and inactive) via " +
			"`systemctl list-units --type=service --all`.",
		Tags:     []string{tagServices},
		Metadata: op("read"),
		Errors:   readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListServicesOutput, error) {
		out, err := oscmd.Run("systemctl", "list-units", "--type=service", "--all", "-o", "json", "--no-pager")
		if err != nil {
			return nil, huma.Error500InternalServerError("systemctl list-units failed", err)
		}
		var units []ServiceUnit
		if err := json.Unmarshal([]byte(out), &units); err != nil {
			return nil, huma.Error500InternalServerError("parse systemctl json failed", err)
		}
		res := &ListServicesOutput{}
		res.Body.Services = units
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "services-get",
		Method:      "GET",
		Path:        "/api/services/{unit}",
		Summary:     "Get a service's status",
		Description: "Returns load/active/sub/unit-file state for one unit via " +
			"`systemctl show`. Returns 404 when the unit does not exist.",
		Tags:     []string{tagServices},
		Metadata: op("read"),
		Errors:   []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *UnitPath) (*GetServiceOutput, error) {
		if err := validateUnit(in.Unit); err != nil {
			return nil, err
		}
		m, err := showUnit(in.Unit)
		if err != nil {
			return nil, huma.Error500InternalServerError("systemctl show failed", err)
		}
		if m["LoadState"] == "not-found" {
			return nil, huma.Error404NotFound("unit not found: " + in.Unit)
		}
		out := &GetServiceOutput{Body: ServiceStatusBody{
			Unit:          m["Id"],
			Description:   m["Description"],
			LoadState:     m["LoadState"],
			ActiveState:   m["ActiveState"],
			SubState:      m["SubState"],
			UnitFileState: m["UnitFileState"],
		}}
		return out, nil
	})

	controls := []struct{ action, summary, desc string }{
		{"start", "Start a service", "Starts the unit (`systemctl start`)."},
		{"stop", "Stop a service", "Stops the unit (`systemctl stop`)."},
		{"restart", "Restart a service", "Restarts the unit (`systemctl restart`)."},
		{"enable", "Enable a service at boot", "Enables the unit (`systemctl enable`)."},
		{"disable", "Disable a service at boot", "Disables the unit (`systemctl disable`)."},
	}
	for _, c := range controls {
		huma.Register(api, huma.Operation{
			OperationID: "services-" + c.action,
			Method:      "POST",
			Path:        "/api/services/{unit}/" + c.action,
			Summary:     c.summary,
			Description: c.desc + " Returns 404 when the unit does not exist.",
			Tags:        []string{tagServices},
			Metadata:    op("write"),
			Errors:      writeErrors,
		}, func(ctx context.Context, in *UnitPath) (*oscmd.StatusOutput, error) {
			if err := validateUnit(in.Unit); err != nil {
				return nil, err
			}
			if err := ensureExists(in.Unit); err != nil {
				return nil, err
			}
			if isSelf(in.Unit) {
				return runDetached(c.action, in.Unit)
			}
			if _, err := oscmd.Run("systemctl", c.action, "--", in.Unit); err != nil {
				return nil, huma.Error500InternalServerError("systemctl "+c.action+" failed", err)
			}
			return oscmd.OK(), nil
		})
	}
}

// isSelf reports whether unit names nadir's own service, with or without the
// .service suffix.
func isSelf(unit string) bool {
	return unit == selfUnit || unit == selfUnit+".service"
}

// runDetached fires systemctl in a new session so a "systemctl restart nadir"
// (or stop) doesn't kill its own caller before the HTTP response is written.
// Returns success once the subprocess has *started* — the actual systemd
// operation may complete after the response is sent, which is the whole point.
func runDetached(action, unit string) (*oscmd.StatusOutput, error) {
	out, err := oscmd.RunDetached("systemctl", action, "--", unit)
	if err != nil {
		return nil, huma.Error500InternalServerError("could not start detached systemctl", err)
	}
	return out, nil
}

// validateUnit guards against empty, flag-like, or malformed unit names.
func validateUnit(unit string) error {
	if unit == "" || strings.HasPrefix(unit, "-") || !unitNameRe.MatchString(unit) {
		return huma.Error400BadRequest("invalid unit name: " + unit)
	}
	return nil
}

// showUnit returns selected properties of a unit as a key=value map. systemctl
// show exits 0 even for unknown units (LoadState=not-found), so callers must
// check LoadState to detect non-existence.
func showUnit(unit string) (map[string]string, error) {
	lines, err := oscmd.RunLines("systemctl", "show",
		"-p", "Id", "-p", "Description", "-p", "LoadState",
		"-p", "ActiveState", "-p", "SubState", "-p", "UnitFileState",
		"--", unit)
	if err != nil {
		return nil, err
	}
	return oscmd.ParseKV(lines), nil
}

// ensureExists returns a 404 if the unit is unknown, mapping the systemctl
// show probe to an HTTP error for the control endpoints.
func ensureExists(unit string) error {
	m, err := showUnit(unit)
	if err != nil {
		return huma.Error500InternalServerError("systemctl show failed", err)
	}
	if m["LoadState"] == "not-found" {
		return huma.Error404NotFound("unit not found: " + unit)
	}
	return nil
}
