package nodes

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"path/filepath"
	"strings"
	"time"
)

// Transport abstracts node data-plane delivery.
type Transport interface {
	Name() string
	Send(ctx context.Context, req Request) (Response, error)
}

// Router prefers p2p transport and falls back to relay.
type Router struct {
	P2P    Transport
	Relay  Transport
	Policy DispatchPolicy
}

func (r *Router) Dispatch(ctx context.Context, req Request, mode string) (Response, error) {
	m := strings.ToLower(strings.TrimSpace(mode))
	if m == "" {
		m = "auto"
	}
	switch m {
	case "p2p":
		if r.P2P == nil {
			return Response{OK: false, Node: req.Node, Action: req.Action, Error: "p2p transport unavailable"}, nil
		}
		resp, err := r.P2P.Send(ctx, req)
		return annotateTransport(resp, "p2p", r.P2P.Name(), ""), err
	case "relay":
		if r.Relay == nil {
			return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay transport unavailable"}, nil
		}
		resp, err := r.Relay.Send(ctx, req)
		return annotateTransport(resp, "relay", r.Relay.Name(), ""), err
	default: // auto
		preferP2P := r.Policy.PreferP2P || r.Relay == nil
		if preferP2P && r.P2P != nil {
			if resp, err := r.P2P.Send(ctx, req); err == nil && resp.OK {
				return annotateTransport(resp, "auto", r.P2P.Name(), ""), nil
			} else if !r.Policy.AllowRelayFallback {
				return annotateTransport(resp, "auto", r.P2P.Name(), ""), err
			}
		}
		if r.Relay != nil {
			resp, err := r.Relay.Send(ctx, req)
			fallback := ""
			if preferP2P && r.P2P != nil {
				fallback = "p2p"
			}
			return annotateTransport(resp, "auto", r.Relay.Name(), fallback), err
		}
		if !preferP2P && r.P2P != nil {
			resp, err := r.P2P.Send(ctx, req)
			return annotateTransport(resp, "auto", r.P2P.Name(), "relay"), err
		}
		return Response{}, fmt.Errorf("no transport available")
	}
}

func annotateTransport(resp Response, mode, usedTransport, fallbackFrom string) Response {
	if resp.Payload == nil {
		resp.Payload = map[string]interface{}{}
	}
	if strings.TrimSpace(mode) != "" {
		resp.Payload["dispatch_mode"] = strings.TrimSpace(mode)
	}
	if strings.TrimSpace(usedTransport) != "" {
		resp.Payload["used_transport"] = strings.TrimSpace(usedTransport)
	}
	if strings.TrimSpace(fallbackFrom) != "" {
		resp.Payload["fallback_from"] = strings.TrimSpace(fallbackFrom)
	}
	return resp
}

// WebsocketP2PTransport uses the persistent node websocket as a request/response tunnel
// while the project evolves toward a true peer data channel.
type WebsocketP2PTransport struct {
	Manager *Manager
}

func (s *WebsocketP2PTransport) Name() string { return "p2p" }
func (s *WebsocketP2PTransport) Send(ctx context.Context, req Request) (Response, error) {
	if s == nil || s.Manager == nil {
		return Response{OK: false, Node: req.Node, Action: req.Action, Error: "p2p manager unavailable"}, nil
	}
	resp, err := s.Manager.SendWireRequest(ctx, req.Node, req)
	if err != nil {
		return Response{OK: false, Code: "p2p_unavailable", Node: req.Node, Action: req.Action, Error: err.Error()}, nil
	}
	resp.Payload = normalizeDevicePayload(resp.Action, resp.Payload)
	return resp, nil
}

// HTTPRelayTransport dispatches requests to node-agent endpoints over HTTP.
type HTTPRelayTransport struct {
	Manager *Manager
	Client  *http.Client
}

func (s *HTTPRelayTransport) Name() string { return "relay" }

var actionHTTPPaths = map[string]string{
	"run":             "/run",
	"invoke":          "/invoke",
	"agent_task":      "/agent/task",
	"camera_snap":     "/camera/snap",
	"camera_clip":     "/camera/clip",
	"screen_record":   "/screen/record",
	"screen_snapshot": "/screen/snapshot",
	"location_get":    "/location/get",
	"canvas_snapshot": "/canvas/snapshot",
	"canvas_action":   "/canvas/action",
}

func actionHTTPPath(action string) string {
	if path := actionHTTPPaths[strings.ToLower(strings.TrimSpace(action))]; path != "" {
		return path
	}
	return "/invoke"
}

func DoEndpointRequest(ctx context.Context, client *http.Client, endpoint, token string, req Request) (Response, error) {
	endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/")
	if endpoint == "" {
		return Response{OK: false, Code: "endpoint_missing", Node: req.Node, Action: req.Action, Error: "node endpoint not configured"}, nil
	}
	if client == nil {
		client = &http.Client{Timeout: 20 * time.Second}
	}
	body, _ := json.Marshal(req)
	path := actionHTTPPath(req.Action)
	hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+path, bytes.NewReader(body))
	if err != nil {
		return Response{}, err
	}
	hreq.Header.Set("Content-Type", "application/json")
	if tok := strings.TrimSpace(token); tok != "" {
		hreq.Header.Set("Authorization", "Bearer "+tok)
	}
	hresp, err := client.Do(hreq)
	if err != nil {
		return Response{OK: false, Code: "transport_error", Node: req.Node, Action: req.Action, Error: err.Error()}, nil
	}
	defer hresp.Body.Close()
	payload, _ := io.ReadAll(io.LimitReader(hresp.Body, 1<<20))
	var resp Response
	if err := json.Unmarshal(payload, &resp); err != nil {
		return Response{OK: false, Code: "invalid_response", Node: req.Node, Action: req.Action, Error: fmt.Sprintf("invalid node response: %s", strings.TrimSpace(string(payload)))}, nil
	}
	if strings.TrimSpace(resp.Node) == "" {
		resp.Node = req.Node
	}
	if strings.TrimSpace(resp.Action) == "" {
		resp.Action = req.Action
	}
	if strings.TrimSpace(resp.Code) == "" {
		if resp.OK {
			resp.Code = "ok"
		} else {
			resp.Code = "remote_error"
		}
	}
	resp.Payload = normalizeDevicePayload(resp.Action, resp.Payload)
	return resp, nil
}

func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, error) {
	if s.Manager == nil {
		return Response{OK: false, Code: "relay_unavailable", Node: req.Node, Action: req.Action, Error: "relay manager not configured"}, nil
	}
	if resp, ok := s.Manager.Invoke(req); ok {
		return resp, nil
	}
	n, ok := s.Manager.Get(req.Node)
	if !ok {
		return Response{OK: false, Code: "node_not_found", Node: req.Node, Action: req.Action, Error: "node not found"}, nil
	}
	return DoEndpointRequest(ctx, s.Client, n.Endpoint, n.Token, req)
}

func normalizeDevicePayload(action string, payload map[string]interface{}) map[string]interface{} {
	if payload == nil {
		payload = map[string]interface{}{}
	}
	a := strings.ToLower(strings.TrimSpace(action))
	switch a {
	case "camera_snap", "screen_snapshot", "canvas_snapshot":
		if _, ok := payload["media_type"]; !ok {
			payload["media_type"] = "image"
		}
	case "camera_clip", "screen_record":
		if _, ok := payload["media_type"]; !ok {
			payload["media_type"] = "video"
		}
	}
	if _, ok := payload["storage"]; !ok {
		if _, hasURL := payload["url"]; hasURL {
			payload["storage"] = "url"
		} else if _, hasPath := payload["path"]; hasPath {
			payload["storage"] = "path"
		} else if _, hasInline := payload["image"]; hasInline {
			payload["storage"] = "inline"
		}
	}
	if _, ok := payload["meta"]; !ok {
		payload["meta"] = map[string]interface{}{}
	}
	payload["artifacts"] = normalizeArtifacts(payload, a)
	return payload
}

func normalizeArtifacts(payload map[string]interface{}, action string) []map[string]interface{} {
	if payload == nil {
		return []map[string]interface{}{}
	}
	if raw, ok := payload["artifacts"]; ok {
		items := normalizeArtifactList(raw)
		if len(items) > 0 {
			return items
		}
	}

	artifact := map[string]interface{}{}
	if mediaType := payloadString(payload, "media_type"); mediaType != "" {
		artifact["kind"] = mediaType
	}
	if mimeType := payloadString(payload, "mime_type"); mimeType != "" {
		artifact["mime_type"] = mimeType
	}
	if storage := payloadString(payload, "storage"); storage != "" {
		artifact["storage"] = storage
	}
	if path := payloadString(payload, "path"); path != "" {
		artifact["path"] = filepath.Clean(path)
	}
	if url := payloadString(payload, "url"); url != "" {
		artifact["url"] = url
	}
	if image := payloadString(payload, "image"); image != "" {
		artifact["content_base64"] = image
	}
	if text := payloadString(payload, "content_text"); text != "" {
		artifact["content_text"] = text
	}
	if name := payloadString(payload, "name"); name != "" {
		artifact["name"] = name
	}
	if size := int64FromPayload(payload["size_bytes"]); size > 0 {
		artifact["size_bytes"] = size
	}
	if len(artifact) == 0 {
		return []map[string]interface{}{}
	}
	if _, ok := artifact["kind"]; !ok && strings.TrimSpace(action) != "" {
		artifact["kind"] = strings.ToLower(strings.TrimSpace(action))
	}
	return []map[string]interface{}{artifact}
}

func normalizeArtifactList(raw interface{}) []map[string]interface{} {
	switch items := raw.(type) {
	case []map[string]interface{}:
		out := make([]map[string]interface{}, 0, len(items))
		for _, item := range items {
			if normalized, ok := normalizeArtifactRow(item); ok {
				out = append(out, normalized)
			}
		}
		return out
	case []interface{}:
		out := make([]map[string]interface{}, 0, len(items))
		for _, item := range items {
			row, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			if normalized, ok := normalizeArtifactRow(row); ok {
				out = append(out, normalized)
			}
		}
		return out
	default:
		return []map[string]interface{}{}
	}
}

func normalizeArtifactRow(row map[string]interface{}) (map[string]interface{}, bool) {
	if len(row) == 0 {
		return nil, false
	}
	normalized := map[string]interface{}{}
	for _, key := range []string{"id", "name", "kind", "mime_type", "storage", "path", "url", "content_text", "content_base64", "source_path"} {
		if value, ok := row[key]; ok && strings.TrimSpace(fmt.Sprint(value)) != "" {
			normalized[key] = value
		}
	}
	if truncated, ok := row["truncated"].(bool); ok && truncated {
		normalized["truncated"] = true
	}
	if size := int64FromPayload(row["size_bytes"]); size > 0 {
		normalized["size_bytes"] = size
	}
	if len(normalized) == 0 {
		return nil, false
	}
	return normalized, true
}

func int64FromPayload(v interface{}) int64 {
	switch value := v.(type) {
	case int:
		return int64(value)
	case int64:
		return value
	case float64:
		return int64(value)
	case json.Number:
		n, _ := value.Int64()
		return n
	case string:
		n, _ := json.Number(strings.TrimSpace(value)).Int64()
		return n
	default:
		return 0
	}
}

func payloadString(payload map[string]interface{}, key string) string {
	if payload == nil {
		return ""
	}
	v, ok := payload[key]
	if !ok {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(v))
}
