// Package dsl 负责把客户端导出的 JSON DSL 解析并组装为可调度的 Task。
package dsl

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/sundynix/sundynix-shared/contract"
)

// ParseAndAssemble 校验 DSL 结构并生成共享契约中的 Task。
func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) {
	if len(raw) == 0 {
		return nil, errors.New("empty dsl")
	}
	// 轻量结构校验：至少要能解析为对象。
	var probe map[string]json.RawMessage
	if err := json.Unmarshal(raw, &probe); err != nil {
		return nil, errors.New("invalid dsl json: " + err.Error())
	}
	// 拓扑校验：前置拦截明确的结构错误（重复/空 id、悬挂边），
	// 避免坏图进编排后被 compose 静默跳过（dangling edge 会被 nodeByID 检查悄悄丢弃）。
	if err := validateTopology(raw); err != nil {
		return nil, err
	}
	return &contract.Task{
		ID:    newID(),
		Graph: raw,
		Meta:  map[string]any{},
	}, nil
}

// validateTopology 做保守的拓扑校验：只拦不含糊的结构错误，对空图/非标准图（如报告任务的
// {topic:...}）与无法按图结构解析的载荷一律宽松放过，杜绝误伤合法提交。
//   - 节点 id 不得为空、不得重复
//   - 每条边的 source/target 不得为空、且必须指向存在的节点（悬挂边直接拒）
func validateTopology(raw json.RawMessage) error {
	var g struct {
		Nodes []struct {
			ID string `json:"id"`
		} `json:"nodes"`
		Edges []struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"edges"`
	}
	if err := json.Unmarshal(raw, &g); err != nil {
		return nil // 非标准图结构（字段类型不匹配）→ 跳过拓扑校验，不误伤
	}
	ids := make(map[string]bool, len(g.Nodes))
	for i, n := range g.Nodes {
		if n.ID == "" {
			return fmt.Errorf("dsl 校验失败：节点[%d] 缺少 id", i)
		}
		if ids[n.ID] {
			return fmt.Errorf("dsl 校验失败：节点 id 重复：%s", n.ID)
		}
		ids[n.ID] = true
	}
	for i, e := range g.Edges {
		if e.Source == "" || e.Target == "" {
			return fmt.Errorf("dsl 校验失败：边[%d] 的 source/target 不能为空", i)
		}
		if !ids[e.Source] {
			return fmt.Errorf("dsl 校验失败：边[%d] 引用不存在的源节点：%s", i, e.Source)
		}
		if !ids[e.Target] {
			return fmt.Errorf("dsl 校验失败：边[%d] 引用不存在的目标节点：%s", i, e.Target)
		}
	}
	return nil
}

func newID() string {
	var b [8]byte
	_, _ = rand.Read(b[:])
	return "task_" + hex.EncodeToString(b[:])
}
