package app

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/dtoro/oikos/internal/core/domain"
	"github.com/dtoro/oikos/internal/core/ports"
	"github.com/google/uuid"
)

// ProvisioningService owns the pct_create use-case: validate + default
// the spec, run the provisioner (SSH-side template resolution, VMID
// guard, preflight, pct create), then register the guest in the entity
// graph. Absorbed the pct_create arm of httpapi's approved-execution
// path (Phase 7); the execution bookkeeping (status rows, events, log
// streaming to the SPA) stays with the calling adapter.
//
// Registration semantics are deliberately best-effort, as before: a
// failed entity/edge write is logged, never fatal — the container exists
// on the host, and the graph can be repaired by re-seeding or manual
// upsert. Failing the execution after a successful create would report
// a provisioning failure for a container that actually exists.
type ProvisioningService struct {
	provisioner ports.Provisioner
	resolver    ports.TargetResolver
	entities    ports.EntityRepository
	rels        ports.RelationshipRepository
}

// NewProvisioningService wires the service.
func NewProvisioningService(
	provisioner ports.Provisioner,
	resolver ports.TargetResolver,
	entities ports.EntityRepository,
	rels ports.RelationshipRepository,
) *ProvisioningService {
	return &ProvisioningService{provisioner: provisioner, resolver: resolver, entities: entities, rels: rels}
}

// CreateLXCCmd is one LXC create request. Only Hostname is required;
// zero fields get lab defaults via DefaultLXCSpec.
type CreateLXCCmd struct {
	HostSlug string // the Proxmox host (host:<slug>) that runs pct
	Hostname string

	VMID         int
	Cores        int
	MemoryMB     int
	DiskGB       int
	IP           string
	GW           string
	Bridge       string
	Storage      string
	Template     string
	Privileged   bool
	Nesting      bool
	Mounts       []string
	Nameserver   string
	Searchdomain string

	// Sink, when non-nil, receives the create command's output chunks as
	// they arrive (execution-log streaming).
	Sink func(stream string, chunk []byte)
}

// DefaultLXCSpec fills the lab defaults. Kept as data (not baked into
// the adapter) so tests and future callers can see and share them.
func DefaultLXCSpec(cmd CreateLXCCmd) ports.LXCInput {
	in := ports.LXCInput{
		HostSlug:     cmd.HostSlug,
		Hostname:     cmd.Hostname,
		VMID:         cmd.VMID,
		Cores:        cmd.Cores,
		MemoryMB:     cmd.MemoryMB,
		DiskGB:       cmd.DiskGB,
		IP:           cmd.IP,
		GW:           cmd.GW,
		Bridge:       cmd.Bridge,
		Storage:      cmd.Storage,
		Template:     cmd.Template,
		Privileged:   cmd.Privileged,
		Nesting:      cmd.Nesting,
		Mounts:       cmd.Mounts,
		Nameserver:   cmd.Nameserver,
		Searchdomain: cmd.Searchdomain,
		Sink:         cmd.Sink,
	}
	if in.Cores == 0 {
		in.Cores = 1
	}
	if in.MemoryMB == 0 {
		in.MemoryMB = 512
	}
	if in.DiskGB == 0 {
		in.DiskGB = 8
	}
	if in.Storage == "" {
		in.Storage = "local-lvm"
	}
	if in.GW == "" {
		in.GW = "192.168.8.2"
	}
	if in.Bridge == "" {
		in.Bridge = "vmbr0"
	}
	if in.Nameserver == "" {
		in.Nameserver = "192.168.8.2"
	}
	if in.Searchdomain == "" {
		in.Searchdomain = "hubris.network"
	}
	return in
}

// LXCOutcome reports a completed create: the graph slug, the VMID the
// cluster actually assigned (may differ from the request), and the
// command output.
type LXCOutcome struct {
	Slug     string
	VMID     int
	Template string
	Output   string
}

// CreateLXC validates the request, provisions the container on the
// resolved Proxmox host, and registers it in the graph. pct_create is
// atomic by design: create + start + register, nothing else — package
// installs and post-install scripts are the agent's own follow-up `run`
// calls so each step is individually observable and recoverable.
func (s *ProvisioningService) CreateLXC(ctx context.Context, cmd CreateLXCCmd) (LXCOutcome, error) {
	if cmd.Hostname == "" {
		return LXCOutcome{}, errors.New("pct_create: hostname is required")
	}

	in := DefaultLXCSpec(cmd)

	addr, user, err := s.resolver.ResolveHost(ctx, in.HostSlug, "")
	if err != nil {
		return LXCOutcome{}, err
	}
	in.HostAddr, in.HostUser = addr, user

	res, err := s.provisioner.CreateLXC(ctx, in)
	if err != nil {
		return LXCOutcome{}, err
	}

	out := LXCOutcome{
		Slug:     "lxc:" + in.Hostname,
		VMID:     res.VMID,
		Template: res.Template,
		Output:   res.Output,
	}
	s.registerLXC(ctx, in, out)
	return out, nil
}

// registerLXC best-effort writes the guest into the graph: entity
// (provisioning state, enrolled now), hosts edge from the Proxmox host,
// and the entity_status row (via the create's derived-checks path).
// Failures are logged, not returned — see the service comment.
func (s *ProvisioningService) registerLXC(ctx context.Context, in ports.LXCInput, out LXCOutcome) {
	id, err := uuid.NewV7()
	if err != nil {
		slog.Error("provisioning: generate entity id", "error", err, "slug", out.Slug)
		return
	}

	now := time.Now()
	attrs := map[string]any{
		"pve_id": fmt.Sprintf("%d", out.VMID),
		"host":   strings.TrimPrefix(in.HostSlug, "host:"),
	}
	if in.IP != "" && !strings.EqualFold(in.IP, "dhcp") {
		attrs["ip"] = in.IP
	}

	guestID := domain.UUID(id.String())
	created, err := s.entities.Create(ctx, ports.EntityCreateInput{
		Entity: domain.Entity{
			ID:         guestID,
			Slug:       out.Slug,
			Type:       "lxc",
			Name:       in.Hostname,
			State:      "provisioning",
			Attributes: attrs,
		},
		EnrolledAt: &now,
	})
	switch {
	case errors.Is(err, domain.ErrAlreadyExists):
		// Re-provision of a known slug: link the edge to the existing row.
		existing, lerr := s.entities.BySlug(ctx, out.Slug)
		if lerr != nil {
			slog.Error("provisioning: resolve existing lxc entity", "error", lerr, "slug", out.Slug)
			return
		}
		guestID = existing.ID
	case err != nil:
		slog.Error("provisioning: lxc entity register", "error", err, "slug", out.Slug)
		return
	default:
		guestID = created.ID
	}

	host, herr := s.entities.BySlug(ctx, in.HostSlug)
	if herr != nil {
		slog.Error("provisioning: resolve host entity for edge", "error", herr, "host", in.HostSlug)
		return
	}
	if _, rerr := s.rels.Create(ctx, ports.RelationshipCreateInput{
		Relationship: domain.Relationship{
			SourceID:   host.ID,
			TargetID:   guestID,
			Type:       "hosts",
			Attributes: map[string]any{"provisioned_by": "nomos"},
		},
	}); rerr != nil && !errors.Is(rerr, domain.ErrAlreadyExists) {
		slog.Error("provisioning: hosts edge register", "error", rerr, "host", in.HostSlug, "lxc", out.Slug)
	}

	slog.Info("provisioning: lxc entity registered", "slug", out.Slug, "vmid", out.VMID, "host", in.HostSlug)
}

// CreateVM provisions a VM via qm on a Proxmox host. Reserved for the
// qm flow; the ssh adapter reports not-implemented until it lands.
func (s *ProvisioningService) CreateVM(ctx context.Context, cmd ports.VMInput) (ports.ProvisionResult, error) {
	addr, user, err := s.resolver.ResolveHost(ctx, cmd.HostSlug, "")
	if err != nil {
		return ports.ProvisionResult{}, err
	}
	cmd.HostAddr, cmd.HostUser = addr, user
	return s.provisioner.CreateVM(ctx, cmd)
}
