package ipc

import (
	"encoding/json"
	"errors"
	"fmt"
)

// Method — one RPC verb. The set is intentionally small: it mirrors exactly
// what a module legitimately needs from core state, and nothing more. Adding
// a method is a core-authority change (every method is a new thing a module
// can ask for); do it deliberately.
type Method string

const (
	MethodWriteFact              Method = "write_fact"
	MethodLatestFact             Method = "latest_fact"
	MethodLatestFactBySource     Method = "latest_fact_by_source"
	MethodSince                  Method = "since"
	MethodPresence               Method = "presence"
	MethodCreateReminder         Method = "create_reminder"
	MethodMarkReminder           Method = "mark_reminder"
	MethodListReminders          Method = "list_reminders"
	MethodRecordNudge            Method = "record_nudge"
	MethodResolveNudge           Method = "resolve_nudge"
	MethodRecentOutcomes         Method = "recent_outcomes"
	MethodRecentFacts            Method = "recent_facts"
	MethodRecentActiveFacts      Method = "recent_active_facts_by_kind"
	MethodCalendarEvents         Method = "calendar_events"
	MethodRecentNudges           Method = "recent_nudges"
	MethodDeliveryAttempts       Method = "delivery_attempts"
	MethodRecentEcoTraces        Method = "recent_ecosystem_traces"
	MethodWriteNote              Method = "write_note"
	MethodQueryNotes             Method = "query_notes"
	MethodRecentNotes            Method = "recent_notes"
	MethodRecentNotesFromSource  Method = "recent_notes_source"
	MethodProposeTool            Method = "propose_tool"
	MethodEnableTool             Method = "enable_tool"
	MethodDisableTool            Method = "disable_tool"
	MethodAssertStepUp           Method = "assert_stepup"
	MethodStoreEncryptionKey     Method = "store_encryption_key"
	MethodUnlock                 Method = "unlock"
	MethodLookupTool             Method = "lookup_tool"
	MethodListTools              Method = "list_tools"
	MethodDeleteTool             Method = "delete_tool"
	MethodListProposedRoutines   Method = "list_proposed_routines"
	MethodDismissProposedRoutine Method = "dismiss_proposed_routine"
	MethodAcceptProposedRoutine  Method = "accept_proposed_routine"
	MethodRevertFact             Method = "revert_fact"
	MethodTickTrace              Method = "tick_trace"
	MethodMorningStatus          Method = "morning_status"
	MethodMCPServers             Method = "mcp_servers"
	MethodDayPlan                Method = "day_plan"
	MethodChat                   Method = "chat"
	MethodCaptureTask            Method = "capture_task"
	MethodListTasks              Method = "list_tasks"
	MethodSetTaskStatus          Method = "set_task_status"
	MethodSetTaskFields          Method = "set_task_fields"
	MethodEditTask               Method = "edit_task"
	MethodResolveEntity          Method = "resolve_entity"
	MethodIngestMail             Method = "ingest_mail"
	MethodSwapModel              Method = "swap_model"
	MethodModelStatus            Method = "model_status"
	MethodDescribeImage          Method = "describe_image"
	MethodCaptureStart           Method = "capture_start"
	MethodCaptureAppend          Method = "capture_append"
	MethodCaptureStop            Method = "capture_stop"
	MethodCaptureStatus          Method = "capture_status"
	MethodEnrollSpeaker          Method = "enroll_speaker"
	MethodListSpeakers           Method = "list_speakers"
	MethodForgetSpeaker          Method = "forget_speaker"
	MethodRecentEvents           Method = "recent_events"
	MethodSeedEvent              Method = "seed_event"

	// MethodPing — liveness, and the only method that answers in locked mode
	// without a passkey assertion. It reaches no store, takes no arguments and
	// returns whether the daemon is locked, so an operator tool can tell "she is
	// up and waiting for a passkey" apart from "she is not there at all".
	// Everything else about her state needs the store, and the store needs the
	// key.
	MethodPing Method = "ping"
)

// Request — one frame from module to core. Params is the JSON-encoded argument
// struct for Method (see api.go for the per-method shapes). The server
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
type Request struct {
	Method Method          `json:"m"`
	Params json.RawMessage `json:"p,omitempty"`
}

// Response — one frame from core back to module. Exactly one of Result/Error
// is set. Result is the JSON-encoded return value of the method (might be a
// scalar, a struct, or null for void methods).
type Response struct {
	Result json.RawMessage `json:"r,omitempty"`
	Error  *RpcError       `json:"e,omitempty"`
}

// RpcError — a typed wire error. Code is one of the sentinel codes below;
// the client rehydrates it into the matching package sentinel so callers can
// use errors.Is like they would in-process (core's contract is the same on
// both sides of the wire — the boundary shouldn't change error semantics).
type RpcError struct {
	Code    string `json:"c"`
	Message string `json:"m,omitempty"`
}

func (e *RpcError) Error() string {
	if e.Message != "" {
		return fmt.Sprintf("ipc: %s: %s", e.Code, e.Message)
	}
	return fmt.Sprintf("ipc: %s", e.Code)
}

// Sentinel codes. Stable over the wire — do not rename. Mirror the package
// sentinels in api.go 1:1. The string is the contract.
const (
	codeNoFact          = "no_fact"
	codeConfidence      = "confidence"
	codeVoidsMissing    = "voids_missing"
	codeNudgeNotFound   = "nudge_not_found"
	codeNudgeOutcome    = "nudge_outcome"
	codeReminderMissing = "reminder_not_found"
	codeReminderState   = "reminder_state"
	codeToolNotFound    = "tool_not_found"
	codeUnknownMethod   = "unknown_method"
	codeBadParams       = "bad_params"
	codeForbidden       = "forbidden"
	codeInternal        = "internal"
)

// codeOf maps a server-side sentinel to its wire code. Anything not matched
// is codeInternal — we never leak internal Go error text to a module; it
// gets a generic "internal" and the daemon logs the real error server-side.
func codeOf(err error) string {
	switch {
	case err == nil:
		return ""
	case errors.Is(err, ErrNoFact):
		return codeNoFact
	case errors.Is(err, ErrConfidence):
		return codeConfidence
	case errors.Is(err, ErrVoidsMissing):
		return codeVoidsMissing
	case errors.Is(err, ErrNudgeNotFound):
		return codeNudgeNotFound
	case errors.Is(err, ErrNudgeOutcome):
		return codeNudgeOutcome
	case errors.Is(err, ErrReminderNotFound):
		return codeReminderMissing
	case errors.Is(err, ErrReminderState):
		return codeReminderState
	case errors.Is(err, ErrToolNotFound):
		return codeToolNotFound
	case errors.Is(err, ErrUnknownMethod):
		return codeUnknownMethod
	case errors.Is(err, ErrBadParams):
		return codeBadParams
	case errors.Is(err, ErrForbidden):
		return codeForbidden
	default:
		return codeInternal
	}
}

// rpcErr builds the wire error for a server-side error. message is omitted
// for sentinel codes (the Code carries the meaning; no need to echo text the
// caller can re-derive from errors.Is) and included for internal/bad-params
// where the text is the actual diagnostic.
func rpcErr(err error) *RpcError {
	c := codeOf(err)
	if c == codeInternal || c == codeBadParams {
		return &RpcError{Code: c, Message: err.Error()}
	}
	return &RpcError{Code: c}
}
