package cli

import (
	"errors"
	"strings"

	"banger/internal/cli/style"
	"banger/internal/rpc"
	"io"
)

// TranslateError is the public entry point used by cmd/banger/main.go
// to render any error reaching the top of the cobra tree. Forwards
// to the package-internal helper so tests can reach it directly.
func TranslateError(w io.Writer, err error) string {
	return translateRPCError(w, err)
}

// translateRPCError turns an error returned by rpc.Call into a
// user-facing string. Known codes get short, friendly prefixes;
// unknown codes pass through verbatim so debuggability is preserved.
// When the daemon attached an op_id the helper appends it in parens
// so an operator can paste it into journalctl --grep.
//
// Color is applied only when w is a TTY (and NO_COLOR is unset).
// The returned string never includes a trailing newline — caller
// chooses where it goes.
func translateRPCError(w io.Writer, err error) string {
	if err == nil {
		return ""
	}
	var rpcErr *rpc.ErrorResponse
	if !errors.As(err, &rpcErr) || rpcErr == nil {
		// Non-RPC failures (dialing the socket, decode errors,
		// context cancellation, ...) come through as plain Go
		// errors. Surface them verbatim — they already mention
		// the underlying cause clearly enough.
		return err.Error()
	}
	prefix := errorCodePrefix(rpcErr.Code)
	body := rpcErr.Message
	if prefix != "" {
		body = prefix + ": " + rpcErr.Message
	} else if rpcErr.Message == "" {
		// Defensive: a server that returned a code with no
		// message still has SOMETHING to report; default to the
		// raw code so we never print an empty error.
		body = rpcErr.Code
	}
	if rpcErr.OpID != "" {
		body = body + " (" + style.Dim(w, rpcErr.OpID) + ")"
	}
	return body
}

// errorCodePrefix maps the small set of codes the daemon emits to
// short user-facing labels. Unknown codes return "" so the message
// alone is shown — keeps the door open for future codes the CLI
// hasn't been updated to recognise.
//
// "operation_failed" is the catch-all the generic dispatcher uses
// when a service returned an error; the message is already self-
// explanatory, so we strip the code entirely. Specialised codes
// (not_found, already_exists, ...) keep a label because the
// message body alone may not say what kind of failure it is.
func errorCodePrefix(code string) string {
	switch strings.TrimSpace(code) {
	case "", "operation_failed":
		return ""
	case "not_found":
		return "not found"
	case "not_running":
		return "not running"
	case "already_exists":
		return "already exists"
	case "bad_request", "bad_params":
		return "bad request"
	case "bad_version":
		return "version mismatch"
	case "unauthorized":
		return "unauthorized"
	case "unknown_method":
		return "unknown method"
	default:
		// Surface the raw code so an operator filing a bug has
		// something concrete to grep for. Strips the boilerplate
		// "operation_failed" but keeps anything novel.
		return code
	}
}
