package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"strings"
)

const (
	hierarchyGraphQueryID       = "hierarchy"
	rustHierarchyLayoutEngineID = "rust_radial_sugiyama"
)

type hierarchyLayoutResult struct {
	Nodes         []Node
	Edges         []Edge
	RouteSegments []RouteSegment
}

type hierarchyLayoutPrepared struct {
	Request         hierarchyLayoutRequest
	NormalizedEdges []Edge
}

type hierarchyLayoutRequest struct {
	RootIRI string                       `json:"root_iri"`
	Nodes   []hierarchyLayoutRequestNode `json:"nodes"`
	Edges   []hierarchyLayoutRequestEdge `json:"edges"`
}

type hierarchyLayoutRequestNode struct {
	NodeID uint32 `json:"node_id"`
	IRI    string `json:"iri"`
}

type hierarchyLayoutRequestEdge struct {
	EdgeIndex    int     `json:"edge_index"`
	ParentID     uint32  `json:"parent_id"`
	ChildID      uint32  `json:"child_id"`
	PredicateIRI *string `json:"predicate_iri,omitempty"`
}

type hierarchyLayoutResponse struct {
	Nodes         []hierarchyLayoutResponseNode         `json:"nodes"`
	RouteSegments []hierarchyLayoutResponseRouteSegment `json:"route_segments"`
}

type hierarchyLayoutResponseNode struct {
	NodeID uint32  `json:"node_id"`
	X      float64 `json:"x"`
	Y      float64 `json:"y"`
	Level  int     `json:"level"`
}

type hierarchyLayoutResponseRouteSegment struct {
	EdgeIndex int                               `json:"edge_index"`
	Kind      string                            `json:"kind"`
	Points    []hierarchyLayoutResponseRoutePoint `json:"points"`
}

type hierarchyLayoutResponseRoutePoint struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}

type hierarchyEdgeKey struct {
	ParentID uint32
	ChildID  uint32
}

func shouldUseRustHierarchyLayout(cfg Config, graphQueryID string) bool {
	return cfg.HierarchyLayoutEngine == "rust" && graphQueryID == hierarchyGraphQueryID
}

func prepareHierarchyLayoutRequest(
	rootIRI string,
	nodes []Node,
	edges []Edge,
	preds *PredicateDict,
) hierarchyLayoutPrepared {
	requestNodes := make([]hierarchyLayoutRequestNode, 0, len(nodes))
	for _, node := range nodes {
		requestNodes = append(requestNodes, hierarchyLayoutRequestNode{
			NodeID: node.ID,
			IRI:    node.IRI,
		})
	}

	predicateIRIs := []string(nil)
	if preds != nil {
		predicateIRIs = preds.IRIs()
	}

	seenEdges := make(map[hierarchyEdgeKey]struct{}, len(edges))
	normalizedEdges := make([]Edge, 0, len(edges))
	requestEdges := make([]hierarchyLayoutRequestEdge, 0, len(edges))
	for _, edge := range edges {
		parentID := edge.Target
		childID := edge.Source
		if parentID == childID {
			continue
		}

		key := hierarchyEdgeKey{ParentID: parentID, ChildID: childID}
		if _, ok := seenEdges[key]; ok {
			continue
		}
		seenEdges[key] = struct{}{}

		normalizedEdges = append(normalizedEdges, edge)

		var predicateIRI *string
		if int(edge.PredicateID) >= 0 && int(edge.PredicateID) < len(predicateIRIs) {
			value := predicateIRIs[edge.PredicateID]
			if strings.TrimSpace(value) != "" {
				predicateIRI = &value
			}
		}

		requestEdges = append(requestEdges, hierarchyLayoutRequestEdge{
			EdgeIndex:    len(normalizedEdges) - 1,
			ParentID:     parentID,
			ChildID:      childID,
			PredicateIRI: predicateIRI,
		})
	}

	return hierarchyLayoutPrepared{
		Request: hierarchyLayoutRequest{
			RootIRI: rootIRI,
			Nodes:   requestNodes,
			Edges:   requestEdges,
		},
		NormalizedEdges: normalizedEdges,
	}
}

func applyHierarchyLayoutResponse(
	nodes []Node,
	normalizedEdges []Edge,
	response hierarchyLayoutResponse,
) (hierarchyLayoutResult, error) {
	positionByID := make(map[uint32]hierarchyLayoutResponseNode, len(response.Nodes))
	for _, node := range response.Nodes {
		if _, ok := positionByID[node.NodeID]; ok {
			return hierarchyLayoutResult{}, fmt.Errorf("hierarchy layout bridge returned duplicate node_id %d", node.NodeID)
		}
		positionByID[node.NodeID] = node
	}

	filteredNodes := make([]Node, 0, len(response.Nodes))
	keptNodeIDs := make(map[uint32]struct{}, len(response.Nodes))
	for _, node := range nodes {
		position, ok := positionByID[node.ID]
		if !ok {
			continue
		}
		node.X = position.X
		node.Y = position.Y
		filteredNodes = append(filteredNodes, node)
		keptNodeIDs[node.ID] = struct{}{}
	}
	if len(filteredNodes) != len(response.Nodes) {
		return hierarchyLayoutResult{}, fmt.Errorf("hierarchy layout bridge returned unknown node ids")
	}

	filteredEdges := make([]Edge, 0, len(normalizedEdges))
	normalizedToFilteredEdge := make(map[int]int, len(normalizedEdges))
	for normalizedIndex, edge := range normalizedEdges {
		if _, ok := keptNodeIDs[edge.Source]; !ok {
			continue
		}
		if _, ok := keptNodeIDs[edge.Target]; !ok {
			continue
		}
		normalizedToFilteredEdge[normalizedIndex] = len(filteredEdges)
		filteredEdges = append(filteredEdges, edge)
	}

	routeSegments := make([]RouteSegment, 0, len(response.RouteSegments))
	for _, segment := range response.RouteSegments {
		filteredEdgeIndex, ok := normalizedToFilteredEdge[segment.EdgeIndex]
		if !ok {
			return hierarchyLayoutResult{}, fmt.Errorf("hierarchy layout bridge returned route for unknown edge_index %d", segment.EdgeIndex)
		}
		points := make([]RoutePoint, 0, len(segment.Points))
		for _, point := range segment.Points {
			points = append(points, RoutePoint{
				X: point.X,
				Y: point.Y,
			})
		}
		routeSegments = append(routeSegments, RouteSegment{
			EdgeIndex: filteredEdgeIndex,
			Kind:      segment.Kind,
			Points:    points,
		})
	}

	return hierarchyLayoutResult{
		Nodes:         filteredNodes,
		Edges:         filteredEdges,
		RouteSegments: routeSegments,
	}, nil
}

func runHierarchyLayoutBridge(
	ctx context.Context,
	cfg Config,
	request hierarchyLayoutRequest,
) (hierarchyLayoutResponse, error) {
	input, err := json.Marshal(request)
	if err != nil {
		return hierarchyLayoutResponse{}, fmt.Errorf("marshal hierarchy layout request failed: %w", err)
	}

	bridgeCtx, cancel := context.WithTimeout(ctx, cfg.HierarchyLayoutTimeout)
	defer cancel()

	cmd := exec.CommandContext(bridgeCtx, cfg.HierarchyLayoutBridgeBin)
	cmd.Dir = cfg.HierarchyLayoutBridgeWorkdir
	cmd.Stdin = bytes.NewReader(input)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		if bridgeCtx.Err() != nil {
			return hierarchyLayoutResponse{}, fmt.Errorf("hierarchy layout bridge timed out after %s", cfg.HierarchyLayoutTimeout)
		}
		detail := strings.TrimSpace(stderr.String())
		if detail == "" {
			detail = err.Error()
		}
		return hierarchyLayoutResponse{}, fmt.Errorf("hierarchy layout bridge failed: %s", detail)
	}

	var response hierarchyLayoutResponse
	if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
		detail := strings.TrimSpace(stderr.String())
		if detail != "" {
			return hierarchyLayoutResponse{}, fmt.Errorf("parse hierarchy layout bridge response failed: %v (stderr: %s)", err, detail)
		}
		return hierarchyLayoutResponse{}, fmt.Errorf("parse hierarchy layout bridge response failed: %w", err)
	}

	return response, nil
}

func layoutHierarchyWithRust(
	ctx context.Context,
	cfg Config,
	nodes []Node,
	edges []Edge,
	preds *PredicateDict,
) (hierarchyLayoutResult, error) {
	prepared := prepareHierarchyLayoutRequest(cfg.HierarchyLayoutRootIRI, nodes, edges, preds)
	response, err := runHierarchyLayoutBridge(ctx, cfg, prepared.Request)
	if err != nil {
		return hierarchyLayoutResult{}, err
	}
	return applyHierarchyLayoutResponse(nodes, prepared.NormalizedEdges, response)
}
