package httpapi

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"strconv"

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

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

	// Check idempotency if a key was provided. The idempotency scope is the
	// calling actor, so replays are per-caller.
	actorType, actor := actorInfo(ctx)
	var idem *ports.Idempotency
	if req.Params.IdempotencyKey != nil && *req.Params.IdempotencyKey != "" {
		bodyJSON, _ := json.Marshal(req.Body)
		hash := fmt.Sprintf("%x", sha256.Sum256(bodyJSON))
		key := *req.Params.IdempotencyKey

		cached, err := s.entityRepo.GetIdempotent(ctx, actor, key)
		if err == nil {
			// Verify the request body hasn't changed.
			if cached.RequestHash != hash {
				return nil, fmt.Errorf("%w: idempotency key %s used with different request body", domain.ErrConflict, key)
			}
			// Replay the cached response.
			if cached.ResponseCode == 201 {
				var entity gen.Entity
				if len(cached.ResponseBody) > 0 {
					if err := json.Unmarshal(cached.ResponseBody, &entity); err != nil {
						return nil, fmt.Errorf("unmarshal cached response: %w", err)
					}
				}
				return gen.CreateEntity201JSONResponse{
					Body:    entity,
					Headers: gen.CreateEntity201ResponseHeaders{ETag: `"` + strconv.Itoa(entity.Version) + `"`},
				}, nil
			}
			// Forward cached error response.
			return gen.CreateEntitydefaultApplicationProblemPlusJSONResponse{
				Body:       gen.Problem{Status: cached.ResponseCode, Title: "replayed error"},
				StatusCode: cached.ResponseCode,
			}, nil
		}
		idem = &ports.Idempotency{
			Actor: actor, Key: key, RequestHash: hash,
			// RenderBody serializes the adapter's wire shape inside the
			// create's transaction, so a replay returns the original
			// response atomically with the insert.
			RenderBody: func(e domain.Entity) []byte {
				b, _ := json.Marshal(domainToGen(e))
				return b
			},
		}
	}

	created, _, err := s.entities.Create(ctx, app.CreateEntityCmd{
		Slug:        req.Body.Slug,
		Type:        req.Body.Type,
		Name:        req.Body.Name,
		State:       derefStr(req.Body.State),
		Attributes:  derefAttrs(req.Body.Attributes),
		ActorType:   actorType,
		Actor:       actor,
		Method:      "POST",
		Path:        "/api/v1/entities",
		Idempotency: idem,
	})
	if err != nil {
		return nil, err
	}
	entity := domainToGen(created)
	return gen.CreateEntity201JSONResponse{
		Body:    entity,
		Headers: gen.CreateEntity201ResponseHeaders{ETag: `"` + strconv.Itoa(entity.Version) + `"`},
	}, nil
}

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

	// Parse If-Match header (quoted version string).
	ifMatch := trimQuotes(req.Params.IfMatch)
	expectedVersion, err := strconv.Atoi(ifMatch)
	if err != nil {
		return nil, fmt.Errorf("%w: invalid If-Match header %q", domain.ErrInvalidInput, req.Params.IfMatch)
	}

	patchActorType, patchActor := actorInfo(ctx)
	updated, _, err := s.entities.Update(ctx, app.UpdateEntityCmd{
		SlugOrID:     req.Id,
		ExpectedVer:  expectedVersion,
		Name:         derefStr(req.Body.Name),
		State:        derefStr(req.Body.State),
		Attributes:   derefAttrs(req.Body.Attributes),
		AttrsReplace: true,
		Maintenance:  req.Body.MaintenanceUntil,
		SetMaint:     req.Body.MaintenanceUntil != nil,
		// Attribute changes propagate to derived checks (the A2 parity fix:
		// previously only the MCP surface regenerated checks on attribute
		// changes).
		RederiveChecks: true,
		ActorType:      patchActorType,
		Actor:          patchActor,
		Method:         "PATCH",
		Path:           "/api/v1/entities/" + req.Id,
	})
	if err != nil {
		return nil, err
	}

	entity := domainToGen(updated)
	s.entityCache.Invalidate(entity.Slug, entity.Id.String())

	return gen.PatchEntity200JSONResponse{
		Body:    entity,
		Headers: gen.PatchEntity200ResponseHeaders{ETag: `"` + strconv.Itoa(entity.Version) + `"`},
	}, nil
}

func derefStr(p *string) string {
	if p == nil {
		return ""
	}
	return *p
}

func derefAttrs(p *map[string]any) map[string]any {
	if p == nil {
		return nil
	}
	return *p
}

func trimQuotes(s string) string {
	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
		return s[1 : len(s)-1]
	}
	return s
}

// domainToGen converts a domain entity to the wire shape.
func domainToGen(e domain.Entity) gen.Entity {
	id, _ := uuid.Parse(string(e.ID))
	out := gen.Entity{
		Id:        id,
		Slug:      e.Slug,
		Type:      e.Type,
		Name:      e.Name,
		Version:   e.Version,
		CreatedAt: e.CreatedAt,
		UpdatedAt: e.UpdatedAt,
	}
	if e.State != "" {
		out.State = &e.State
	}
	if e.MaintenanceUntil != nil {
		out.MaintenanceUntil = e.MaintenanceUntil
	}
	if len(e.Attributes) > 0 {
		attrs := e.Attributes
		out.Attributes = &attrs
	}
	return out
}
