package cli

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"iop/apps/node/internal/runtime"
)

type opencodeEnvelope struct {
	Type       string         `json:"type"`
	Properties map[string]any `json:"properties"`
}

type opencodeGlobalEnvelope struct {
	Payload *opencodeEnvelope `json:"payload"`
}

func opencodeEventEnvelope(data []byte) (opencodeEnvelope, bool) {
	var env opencodeEnvelope
	if err := json.Unmarshal(data, &env); err == nil && env.Type != "" && env.Type != "sync" {
		return env, true
	}
	var global opencodeGlobalEnvelope
	if err := json.Unmarshal(data, &global); err != nil || global.Payload == nil || global.Payload.Type == "" || global.Payload.Type == "sync" {
		return opencodeEnvelope{}, false
	}
	return *global.Payload, true
}

type opencodeEventHandler func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error)

var opencodeEventHandlers = map[string]opencodeEventHandler{
	"session.next.text.delta": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		delta := opencodeDeltaText(ev.Properties)
		if delta != "" {
			r.outputTokens += len(strings.Fields(delta))
			_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
				RunID:     r.spec.RunID,
				Type:      runtime.EventTypeDelta,
				Delta:     delta,
				Timestamp: time.Now(),
			})
			r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "delta"})
		} else {
			r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"})
		}
		return false, nil
	},
	"message.part.updated": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		if partID, partType, ok := opencodePartInfo(ev.Properties); ok {
			r.partTypes[partID] = partType
		}
		if in, out, ok := opencodeStepTokens(ev.Properties); ok {
			if in > 0 {
				r.inputTokens = in
			}
			if out > 0 {
				r.outputTokens = out
			}
		}
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"})
		return false, nil
	},
	"message.part.delta": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		delta := opencodeMessagePartDeltaText(ev.Properties, r.partTypes)
		if delta != "" {
			r.outputTokens += len(strings.Fields(delta))
			_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
				RunID:     r.spec.RunID,
				Type:      runtime.EventTypeDelta,
				Delta:     delta,
				Timestamp: time.Now(),
			})
			r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "delta"})
		} else {
			r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"})
		}
		return false, nil
	},
	"session.next.step.ended": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		if in, out, ok := opencodeStepTokens(ev.Properties); ok {
			if in > 0 {
				r.inputTokens = in
			}
			if out > 0 {
				r.outputTokens = out
			}
		}
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"})
		return false, nil
	},
	"session.idle": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "idle"})
		return true, emitOpencodeComplete(r.ctx, r.sink, r.spec.RunID, r.inputTokens, r.outputTokens)
	},
	"session.status": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		if opencodeStatusIdle(ev.Properties) {
			r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "idle"})
			return true, emitOpencodeComplete(r.ctx, r.sink, r.spec.RunID, r.inputTokens, r.outputTokens)
		}
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"})
		return false, nil
	},
	"session.error": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		msg := opencodeErrorMessage(ev.Properties)
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "error"})
		_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
			RunID:     r.spec.RunID,
			Type:      runtime.EventTypeError,
			Error:     msg,
			Timestamp: time.Now(),
		})
		return true, fmt.Errorf("cli adapter: opencode session error: %s", msg)
	},
	"permission.asked": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) {
		permID, _ := stringFromProp(ev.Properties, "id")
		if permID == "" {
			permID, _ = stringFromProp(ev.Properties, "permissionID")
		}
		if permID != "" {
			reply := "reject"
			if r.opts.DangerouslySkipPermissions {
				reply = "once"
			}
			bg, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			_ = opencodePermissionReply(bg, r.sess.serverURL, permID, reply)
			cancel()
		}
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"})
		return false, nil
	},
}

func (r *opencodeSSERun) handleEvent(ev opencodeEnvelope, sidStatus string) (bool, error) {
	handler := opencodeEventHandlers[ev.Type]
	if handler == nil {
		r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"})
		return false, nil
	}
	return handler(r, ev, sidStatus)
}

func stringFromProp(props map[string]any, key string) (string, bool) {
	if props == nil {
		return "", false
	}
	v, ok := props[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}

func opencodeDeltaText(props map[string]any) string {
	if props == nil {
		return ""
	}
	if s, ok := props["delta"].(string); ok && s != "" {
		return s
	}
	if part, ok := props["part"].(map[string]any); ok {
		if s, ok := part["text"].(string); ok {
			return s
		}
	}
	return ""
}

func opencodePartInfo(props map[string]any) (string, string, bool) {
	if props == nil {
		return "", "", false
	}
	part, ok := props["part"].(map[string]any)
	if !ok {
		return "", "", false
	}
	id, _ := part["id"].(string)
	partType, _ := part["type"].(string)
	if id == "" || partType == "" {
		return "", "", false
	}
	return id, partType, true
}

func opencodeMessagePartDeltaText(props map[string]any, partTypes map[string]string) string {
	if props == nil {
		return ""
	}
	field, _ := props["field"].(string)
	if field != "text" {
		return ""
	}
	partID, _ := props["partID"].(string)
	if partID != "" {
		if pt, known := partTypes[partID]; known && pt != "text" {
			return ""
		}
	}
	delta, _ := props["delta"].(string)
	return delta
}

func opencodeStepTokens(props map[string]any) (int, int, bool) {
	if props == nil {
		return 0, 0, false
	}
	tokens, ok := props["tokens"].(map[string]any)
	if !ok {
		if part, partOK := props["part"].(map[string]any); partOK {
			tokens, ok = part["tokens"].(map[string]any)
		}
	}
	if !ok {
		return 0, 0, false
	}
	in := intFromNumber(tokens["input"])
	out := intFromNumber(tokens["output"])
	return in, out, true
}

func intFromNumber(v any) int {
	switch n := v.(type) {
	case float64:
		return int(n)
	case int:
		return n
	case json.Number:
		if i, err := n.Int64(); err == nil {
			return int(i)
		}
	}
	return 0
}

func emitOpencodeComplete(ctx context.Context, sink runtime.EventSink, runID string, inputTokens, outputTokens int) error {
	return sink.Emit(ctx, runtime.RuntimeEvent{
		RunID:   runID,
		Type:    runtime.EventTypeComplete,
		Message: "opencode sse execution complete",
		Usage: &runtime.UsageStats{
			InputTokens:  inputTokens,
			OutputTokens: outputTokens,
		},
		Timestamp: time.Now(),
	})
}

func opencodeModelPayload(s string) (map[string]any, error) {
	idx := strings.Index(s, "/")
	if idx < 0 {
		return nil, fmt.Errorf("cli adapter: opencode model %q missing provider/model separator", s)
	}
	provider := s[:idx]
	model := s[idx+1:]
	if provider == "" || model == "" {
		return nil, fmt.Errorf("cli adapter: opencode model %q has empty provider or model", s)
	}
	return map[string]any{"providerID": provider, "modelID": model}, nil
}

func opencodeStatusIdle(props map[string]any) bool {
	if props == nil {
		return false
	}
	switch v := props["status"].(type) {
	case string:
		return v == "idle"
	case map[string]any:
		t, _ := v["type"].(string)
		return t == "idle"
	}
	return false
}

func opencodeErrorMessage(props map[string]any) string {
	if props == nil {
		return "opencode error"
	}
	if errObj, ok := props["error"].(map[string]any); ok {
		if data, ok := errObj["data"].(map[string]any); ok {
			if m, ok := data["message"].(string); ok && m != "" {
				return m
			}
		}
		if name, ok := errObj["name"].(string); ok && name != "" {
			return name
		}
	}
	if m, ok := props["message"].(string); ok && m != "" {
		return m
	}
	return "opencode error"
}
