package acp

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"sync"
	"sync/atomic"
)

type rpcOutcome struct {
	result json.RawMessage
	err    *rpcErrPayload
}

type rpcErrPayload struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func (e *rpcErrPayload) Error() string {
	if e == nil {
		return "json-rpc error"
	}
	return fmt.Sprintf("json-rpc %d: %s", e.Code, e.Message)
}

type rpcNotifyHandler func(method string, params json.RawMessage)
type rpcRequestHandler func(method string, id json.RawMessage, params json.RawMessage)

// transport implements newline-delimited JSON-RPC 2.0 over a pair of streams.
type transport struct {
	in  *bufio.Reader
	out io.Writer
	mu  sync.Mutex
	enc *json.Encoder

	nextID atomic.Int64

	pendingMu sync.Mutex
	pending   map[string]chan rpcOutcome

	onNotif rpcNotifyHandler
	onReq   rpcRequestHandler
}

func newTransport(in io.Reader, out io.Writer, onNotif rpcNotifyHandler, onReq rpcRequestHandler) *transport {
	return &transport{
		in:      bufio.NewReader(in),
		out:     out,
		enc:     json.NewEncoder(out),
		pending: make(map[string]chan rpcOutcome),
		onNotif: onNotif,
		onReq:   onReq,
	}
}

func (t *transport) readLoop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}
		line, err := t.readLine()
		if err != nil {
			if err != io.EOF {
				slog.Debug("acp: read error", "error", err)
			}
			t.cancelAll(fmt.Errorf("acp: read closed: %w", err))
			return
		}
		if len(bytes.TrimSpace(line)) == 0 {
			continue
		}
		t.dispatchLine(line)
	}
}

func (t *transport) readLine() ([]byte, error) {
	line, err := t.in.ReadBytes('\n')
	if err != nil {
		return line, err
	}
	return bytes.TrimSuffix(line, []byte("\r")), nil
}

func (t *transport) dispatchLine(line []byte) {
	var env struct {
		JSONRPC string          `json:"jsonrpc"`
		ID      json.RawMessage `json:"id"`
		Method  string          `json:"method"`
		Params  json.RawMessage `json:"params"`
		Result  json.RawMessage `json:"result"`
		Error   *rpcErrPayload  `json:"error"`
	}
	if err := json.Unmarshal(line, &env); err != nil {
		slog.Debug("acp: skip non-json line", "line", string(line))
		return
	}
	if env.Method != "" {
		if isJSONRPCIDNullOrAbsent(env.ID) {
			if t.onNotif != nil {
				t.onNotif(env.Method, env.Params)
			}
			return
		}
		if t.onReq != nil {
			t.onReq(env.Method, env.ID, env.Params)
		}
		return
	}
	if !isJSONRPCIDNullOrAbsent(env.ID) {
		t.completePending(env.ID, env.Result, env.Error)
	}
}

func isJSONRPCIDNullOrAbsent(id json.RawMessage) bool {
	if len(id) == 0 {
		return true
	}
	return bytes.Equal(bytes.TrimSpace(id), []byte("null"))
}

func jsonIDKey(id json.RawMessage) string {
	id = bytes.TrimSpace(id)
	var n json.Number
	if json.Unmarshal(id, &n) == nil {
		return string(n)
	}
	var s string
	if json.Unmarshal(id, &s) == nil {
		return s
	}
	return string(id)
}

func (t *transport) completePending(id json.RawMessage, result json.RawMessage, rpcErr *rpcErrPayload) {
	key := jsonIDKey(id)
	t.pendingMu.Lock()
	ch, ok := t.pending[key]
	delete(t.pending, key)
	t.pendingMu.Unlock()
	if !ok {
		slog.Debug("acp: unmatched rpc response", "id", key)
		return
	}
	select {
	case ch <- rpcOutcome{result: result, err: rpcErr}:
	default:
	}
}

func (t *transport) cancelAll(err error) {
	t.pendingMu.Lock()
	defer t.pendingMu.Unlock()
	msg := err.Error()
	for k, ch := range t.pending {
		select {
		case ch <- rpcOutcome{err: &rpcErrPayload{Code: -32000, Message: msg}}:
		default:
		}
		delete(t.pending, k)
	}
}

func (t *transport) call(ctx context.Context, method string, params any) (json.RawMessage, error) {
	id := t.nextID.Add(1)
	key := fmt.Sprintf("%d", id)
	ch := make(chan rpcOutcome, 1)
	t.pendingMu.Lock()
	t.pending[key] = ch
	t.pendingMu.Unlock()

	req := map[string]any{
		"jsonrpc": "2.0",
		"id":      id,
		"method":  method,
		"params":  params,
	}
	if err := t.writeJSON(req); err != nil {
		t.pendingMu.Lock()
		delete(t.pending, key)
		t.pendingMu.Unlock()
		return nil, fmt.Errorf("acp: write %s: %w", method, err)
	}
	select {
	case <-ctx.Done():
		t.pendingMu.Lock()
		delete(t.pending, key)
		t.pendingMu.Unlock()
		return nil, ctx.Err()
	case out := <-ch:
		if out.err != nil {
			return nil, out.err
		}
		return out.result, nil
	}
}

func (t *transport) sendNotification(method string, params any) error {
	return t.writeJSON(map[string]any{
		"jsonrpc": "2.0",
		"method":  method,
		"params":  params,
	})
}

func (t *transport) writeJSON(v any) error {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.enc.Encode(v)
}

type rpcResponseMsg struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      json.RawMessage `json:"id"`
	Result  any             `json:"result,omitempty"`
	Error   *rpcErrPayload  `json:"error,omitempty"`
}

func (t *transport) respondSuccess(id json.RawMessage, result any) error {
	return t.writeJSON(rpcResponseMsg{JSONRPC: "2.0", ID: id, Result: result})
}

func (t *transport) respondError(id json.RawMessage, code int, message string) error {
	return t.writeJSON(rpcResponseMsg{
		JSONRPC: "2.0",
		ID:      id,
		Error:   &rpcErrPayload{Code: code, Message: message},
	})
}
