package httpapi

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"math/big"
	"strconv"
	"time"

	"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
	"github.com/dtoro/oikos/internal/core/domain"
	"github.com/dtoro/oikos/internal/httpapi/gen"
	"github.com/dtoro/oikos/internal/observability"
	"github.com/google/uuid"
	openapi_types "github.com/oapi-codegen/runtime/types"
)

func (s *Server) EnrollClient(ctx context.Context, req gen.EnrollClientRequestObject) (gen.EnrollClientResponseObject, error) {
	if req.Body == nil {
		return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
	}

	id, err := s.resolveEntityID(ctx, req.Body.Slug)
	if err != nil {
		return nil, err
	}

	current, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("%w: %s", domain.ErrNotFound, req.Body.Slug)
	}

	currentState := ""
	if current.State != nil {
		currentState = *current.State
	}
	if currentState != "planned" && currentState != "provisioning" {
		return nil, fmt.Errorf("%w: entity %s is in state %q, expected planned or provisioning",
			domain.ErrInvalidTransition, req.Body.Slug, currentState)
	}

	meshIP := ""
	if req.Body.MeshIp != nil {
		meshIP = *req.Body.MeshIp
	}
	if meshIP == "" {
		return nil, fmt.Errorf("%w: mesh_ip is required for enrollment", domain.ErrInvalidInput)
	}

	agePubKey, agePrivKey, err := generateAgeKeypair()
	if err != nil {
		return nil, fmt.Errorf("age key generation: %w", err)
	}

	if s.secretsManager != nil {
		keyPath := "clients/" + req.Body.Slug + "/age-key"
		_ = s.secretsManager.Set(ctx, keyPath, agePrivKey)
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	var attrs map[string]any
	if len(current.Attributes) > 0 {
		json.Unmarshal(current.Attributes, &attrs)
	}
	if attrs == nil {
		attrs = map[string]any{}
	}
	attrs["age_pubkey"] = agePubKey
	attrs["mesh_ip"] = meshIP
	attrs["enrolled_at"] = time.Now().UTC().Format(time.RFC3339)
	if req.Body.Hostname != nil {
		attrs["hostname"] = *req.Body.Hostname
	}
	attrsJSON, _ := json.Marshal(attrs)

	q := sqlcgen.New(tx)
	provisioning := "provisioning"
	now := time.Now().UTC()
	_, err = q.UpdateEntity(ctx, sqlcgen.UpdateEntityParams{
		State:      &provisioning,
		Attributes: attrsJSON,
		ID:         id,
		Version:    current.Version,
	})
	if err != nil {
		return nil, err
	}

	_, _ = tx.Exec(ctx,
		"UPDATE entities SET enrolled_at = $1 WHERE id = $2", now, id)

	_, actor := actorInfo(ctx)
	entityID := id
	_ = observability.Audit(ctx, q, "operator", actor, "enroll",
		&entityID, "POST", "/api/v1/clients/enroll", "",
		nil,
		map[string]any{"slug": req.Body.Slug, "mesh_ip": meshIP})
	_ = observability.Event(ctx, q, "client.enrolled", &entityID,
		"info", "oikos-api", "",
		map[string]any{"slug": req.Body.Slug, "type": current.Type})

	if err := ensureDefaultChecks(ctx, tx, id, req.Body.Slug, current.Type, current.Name, attrsJSON); err != nil {
		return nil, err
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	// Store age key in Infisical when backend is available.
	if s.secretsManager != nil {
		keyPath := "clients/" + req.Body.Slug + "/age-key"
		_ = s.secretsManager.Set(ctx, keyPath, agePrivKey)
	}

	resp := gen.EnrollResponse{
		AgePublicKey:  agePubKey,
		AgePrivateKey: agePrivKey,
	}

	return gen.EnrollClient200JSONResponse(resp), nil
}

func (s *Server) ProvisionEntity(ctx context.Context, req gen.ProvisionEntityRequestObject) (gen.ProvisionEntityResponseObject, error) {
	if req.Body == nil {
		return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
	}

	hostSlug := req.Body.Host
	hostID, err := s.resolveEntityID(ctx, hostSlug)
	if err != nil {
		return nil, fmt.Errorf("%w: host %q not found", domain.ErrNotFound, hostSlug)
	}

	var existingID uuid.UUID
	err = s.pool.QueryRow(ctx,
		"SELECT id FROM entities WHERE slug = $1", req.Body.Slug).Scan(&existingID)
	if err == nil {
		return nil, fmt.Errorf("%w: entity slug %q already exists", domain.ErrConflict, req.Body.Slug)
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	entityID := uuid.Must(uuid.NewV7())
	var attrsJSON []byte
	if req.Body.Attributes != nil {
		attrsJSON, _ = json.Marshal(req.Body.Attributes)
	}
	if len(attrsJSON) == 0 {
		attrsJSON = []byte("{}")
	}

	plannedState := "planned"
	q := sqlcgen.New(tx)
	inserted, err := q.InsertEntity(ctx, sqlcgen.InsertEntityParams{
		ID:         entityID,
		Slug:       req.Body.Slug,
		Type:       req.Body.Type,
		Name:       req.Body.Name,
		State:      &plannedState,
		Attributes: attrsJSON,
	})
	if err != nil {
		return nil, err
	}

	execID := uuid.Must(uuid.NewV7())
	corrID := "provision_" + entityID.String()[:8]
	if err := q.InsertExecution(ctx, sqlcgen.InsertExecutionParams{
		EntityID:      entityID,
		Action:        "provision",
		RiskClass:     "config_mutation",
		CorrelationID: corrID,
	}); err != nil {
		return nil, fmt.Errorf("create execution: %w", err)
	}

	type stepDef struct {
		order int
		name  string
	}
	steps := []stepDef{
		{1, "validate-constraints"},
		{2, "create-container"},
		{3, "configure-network"},
		{4, "install-services"},
		{5, "configure-mounts"},
		{6, "health-check"},
	}
	for _, st := range steps {
		_, err = tx.Exec(ctx,
			`INSERT INTO provisioning_steps (id, entity_id, execution_id, step_order, step_name)
			 VALUES ($1, $2, $3, $4, $5)`,
			uuid.Must(uuid.NewV7()), entityID, entityID /* executions PK is entity_id */, st.order, st.name)
		if err != nil {
			return nil, fmt.Errorf("insert provisioning step: %w", err)
		}
	}

	_, err = tx.Exec(ctx,
		`INSERT INTO relationships (source_id, target_id, type)
		 VALUES ($1, $2, 'hosts')`, hostID, entityID)
	if err != nil {
		return nil, fmt.Errorf("insert relationship: %w", err)
	}

	_, actor := actorInfo(ctx)
	_ = observability.Audit(ctx, q, "operator", actor, "provision",
		&entityID, "POST", "/api/v1/entities/provision", "",
		nil,
		map[string]any{"slug": req.Body.Slug, "host": hostSlug})
	_ = observability.Event(ctx, q, "entity.provisioned", &entityID,
		"info", "oikos-api", "",
		map[string]any{"slug": req.Body.Slug, "type": req.Body.Type, "host": hostSlug})

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	entity := sqlcEntityToGen(inserted)
	return gen.ProvisionEntity201JSONResponse{
		Body: gen.ProvisionResponse{
			Entity:      entity,
			ExecutionId: openapi_types.UUID(execID),
		},
		Headers: gen.ProvisionEntity201ResponseHeaders{ETag: `"` + strconv.Itoa(int(inserted.Version)) + `"`},
	}, nil
}

func (s *Server) GetProvisionStatus(ctx context.Context, req gen.GetProvisionStatusRequestObject) (gen.GetProvisionStatusResponseObject, error) {
	slug := string(req.Slug)
	id, err := s.resolveEntityID(ctx, slug)
	if err != nil {
		return nil, err
	}

	var state string
	if err := s.pool.QueryRow(ctx,
		"SELECT state FROM entities WHERE id = $1", id).Scan(&state); err != nil {
		return nil, fmt.Errorf("%w: %s", domain.ErrNotFound, slug)
	}

	rows, err := s.pool.Query(ctx,
		`SELECT step_name, status, error_message, started_at, finished_at
		 FROM provisioning_steps WHERE entity_id = $1 ORDER BY step_order`, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var provSteps []struct {
		ErrorMessage *string                        `json:"error_message"`
		FinishedAt   *time.Time                     `json:"finished_at"`
		StartedAt    *time.Time                     `json:"started_at"`
		Status       gen.ProvisionStatusStepsStatus `json:"status"`
		Step         string                         `json:"step"`
	}
	for rows.Next() {
		var stepName, status string
		var errMsg *string
		var started, finished *time.Time
		if scanErr := rows.Scan(&stepName, &status, &errMsg, &started, &finished); scanErr != nil {
			return nil, scanErr
		}
		provSteps = append(provSteps, struct {
			ErrorMessage *string                        `json:"error_message"`
			FinishedAt   *time.Time                     `json:"finished_at"`
			StartedAt    *time.Time                     `json:"started_at"`
			Status       gen.ProvisionStatusStepsStatus `json:"status"`
			Step         string                         `json:"step"`
		}{
			Step:         stepName,
			Status:       gen.ProvisionStatusStepsStatus(status),
			ErrorMessage: errMsg,
			StartedAt:    started,
			FinishedAt:   finished,
		})
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	return gen.GetProvisionStatus200JSONResponse{
		Slug:  slug,
		State: state,
		Steps: provSteps,
	}, nil
}

func generateAgeKeypair() (pubKey, privKey string, err error) {
	seed := make([]byte, 32)
	if _, err := rand.Read(seed); err != nil {
		return "", "", err
	}
	n := new(big.Int).SetBytes(seed)
	pub := fmt.Sprintf("age1%064x", n)
	priv := fmt.Sprintf("AGE-SECRET-KEY-1%064x", n)
	return pub, priv, nil
}