package httpapi

import (
	"context"
	"encoding/json"
	"strconv"

	"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
	"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) ListEntities(ctx context.Context, req gen.ListEntitiesRequestObject) (gen.ListEntitiesResponseObject, error) {
	limit := clampLimit(req.Params.Limit)
	filters := ports.EntityFilters{
		Type:   strOrEmpty(req.Params.Type),
		State:  strOrEmpty(req.Params.State),
		Q:      strOrEmpty(req.Params.Q),
		Domain: strOrEmpty(req.Params.Domain),
		Layer:  strOrEmpty(req.Params.Layer),
		Cursor: strOrEmpty(req.Params.Cursor),
		Limit:  limit,
	}

	items, next, err := s.readModels.ListEntities(ctx, filters)
	if err != nil {
		return nil, err
	}

	entities := make([]gen.Entity, len(items))
	for i, e := range items {
		entities[i] = entityWithHealthToGen(e)
	}
	if entities == nil {
		entities = []gen.Entity{}
	}
	return gen.ListEntities200JSONResponse{Items: entities, NextCursor: &next}, nil
}

func (s *Server) GetEntity(ctx context.Context, req gen.GetEntityRequestObject) (gen.GetEntityResponseObject, error) {
	uid, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}
	e, err := s.readModels.GetEntity(ctx, domain.UUID(uid.String()))
	if err != nil {
		return nil, err
	}
	entity := entityWithHealthToGen(e)
	return gen.GetEntity200JSONResponse{
		Body:    entity,
		Headers: gen.GetEntity200ResponseHeaders{ETag: `"` + strconv.Itoa(e.Entity.Version) + `"`},
	}, nil
}

func (s *Server) GetEntityRelations(ctx context.Context, req gen.GetEntityRelationsRequestObject) (gen.GetEntityRelationsResponseObject, error) {
	uid, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}
	dir := "both"
	if req.Params.Direction != nil {
		dir = string(*req.Params.Direction)
	}
	relType := strOrEmpty(req.Params.RelType)

	rels, err := s.readModels.GetEntityRelations(ctx, domain.UUID(uid.String()), dir, relType)
	if err != nil {
		return nil, err
	}

	items := make([]gen.Relationship, len(rels))
	for i, r := range rels {
		items[i] = relToGen(r)
	}
	return gen.GetEntityRelations200JSONResponse{Items: items}, nil
}

func (s *Server) GetBlastRadius(ctx context.Context, req gen.GetBlastRadiusRequestObject) (gen.GetBlastRadiusResponseObject, error) {
	uid, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}
	depth := 3
	if req.Params.Depth != nil {
		depth = *req.Params.Depth
	}

	items, err := s.readModels.GetBlastRadius(ctx, domain.UUID(uid.String()), depth)
	if err != nil {
		return nil, err
	}

	resp := gen.GetBlastRadius200JSONResponse{Items: []struct {
		Depth  int        `json:"depth"`
		Entity gen.Entity `json:"entity"`
	}{}}
	for _, e := range items {
		resp.Items = append(resp.Items, struct {
			Depth  int        `json:"depth"`
			Entity gen.Entity `json:"entity"`
		}{Depth: 0, Entity: entityWithHealthToGen(e)})
	}
	return resp, nil
}

func (s *Server) GetGraph(ctx context.Context, req gen.GetGraphRequestObject) (gen.GetGraphResponseObject, error) {
	depth := 2
	if req.Params.Depth != nil {
		depth = *req.Params.Depth
	}

	var rootID *domain.UUID
	if req.Params.Root != nil && *req.Params.Root != "" {
		uid, err := s.resolveEntityID(ctx, *req.Params.Root)
		if err != nil {
			return nil, err
		}
		duid := domain.UUID(uid.String())
		rootID = &duid
	}

	var relTypes []string
	if req.Params.RelType != nil {
		relTypes = *req.Params.RelType
	}

	nodes, edges, truncated, err := s.readModels.GetGraph(ctx, depth, rootID, relTypes, graphNodeCap)
	if err != nil {
		return nil, err
	}

	genNodes := make([]gen.Entity, len(nodes))
	for i, n := range nodes {
		genNodes[i] = entityWithHealthToGen(n)
	}
	genEdges := make([]gen.Relationship, len(edges))
	for i, e := range edges {
		genEdges[i] = relToGen(e)
	}

	resp := gen.GetGraph200JSONResponse{Nodes: genNodes, Edges: genEdges}
	if truncated {
		resp.Truncated = &truncated
	}

	if req.Params.Include != nil {
		for _, inc := range *req.Params.Include {
			if inc == gen.Status {
				health := make(map[string]gen.GraphViewHealth, len(nodes))
				for _, n := range nodes {
					health[string(n.Entity.ID)] = gen.GraphViewHealth(n.Health)
				}
				resp.Health = &health
				break
			}
		}
	}
	return resp, nil
}

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

func entityWithHealthToGen(e ports.EntityWithHealth) gen.Entity {
	id, _ := uuid.Parse(string(e.Entity.ID))
	out := gen.Entity{
		Id:        id,
		Slug:      e.Entity.Slug,
		Type:      e.Entity.Type,
		Name:      e.Entity.Name,
		Version:   e.Entity.Version,
		CreatedAt: e.Entity.CreatedAt,
		UpdatedAt: e.Entity.UpdatedAt,
	}
	if e.Entity.State != "" {
		out.State = &e.Entity.State
	}
	if e.Entity.MaintenanceUntil != nil {
		out.MaintenanceUntil = e.Entity.MaintenanceUntil
	}
	if len(e.Entity.Attributes) > 0 {
		attrs := e.Entity.Attributes
		out.Attributes = &attrs
	}
	if e.Health != "" {
		h := gen.EntityHealth(e.Health)
		out.Health = &h
	}
	out.LastCheckAt = e.LastCheckAt
	return out
}

func relToGen(r domain.Relationship) gen.Relationship {
	var attrs *map[string]any
	if len(r.Attributes) > 0 {
		attrs = &r.Attributes
	}
	return gen.Relationship{
		Source:     string(r.SourceID),
		Target:     string(r.TargetID),
		Type:       r.Type,
		Attributes: attrs,
		ValidFrom:  r.ValidFrom,
		ValidTo:    r.ValidTo,
	}
}

// sqlcEntityToGen converts a sqlcgen entity to the wire shape. Used by
// client_lifecycle.go (enroll) — the mutation handlers use domainToGen.
func sqlcEntityToGen(e sqlcgen.Entity) gen.Entity {
	out := gen.Entity{
		Id:        e.ID,
		Slug:      e.Slug,
		Type:      e.Type,
		Name:      e.Name,
		State:     e.State,
		Version:   int(e.Version),
		CreatedAt: e.CreatedAt,
		UpdatedAt: e.UpdatedAt,
	}
	if e.MaintenanceUntil != nil {
		out.MaintenanceUntil = e.MaintenanceUntil
	}
	if len(e.Attributes) > 0 {
		var attrs map[string]any
		if json.Unmarshal(e.Attributes, &attrs) == nil && len(attrs) > 0 {
			out.Attributes = &attrs
		}
	}
	return out
}