// Chowdahh MCP server — stdio JSON-RPC transport, thin translation layer over
// https://chowdahh.com/api/v1. See README.md for installation.
//
// Auth: pass CHOWDAHH_KEY in the environment. Token is sent as
// `Authorization: Bearer …` on every request — reads and writes — so the
// secret never appears in URLs, server access logs, CDN logs, Referer
// headers, or reflected error bodies. ADR-0140's `?key=` form is for
// browser-paste-into-LLM flows where the agent can only carry a URL; the
// MCP server has no such constraint.
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"time"
)

// Defense in depth: if a future server bug ever reflects a Chowdahh-shaped
// token back to us (e.g. echoing a request header in a 400 response body),
// scrub it out before we hand the bytes to the LLM. The server never puts
// tokens in URLs, so this is belt-and-braces.
var tokenRe = regexp.MustCompile(`(ch_person_|ch_cur_|ohp_pat_)[A-Za-z0-9_-]+`)

func redact(b []byte) []byte {
	return tokenRe.ReplaceAll(b, []byte("${1}REDACTED"))
}

const apiBase = "https://chowdahh.com/api/v1"

func main() {
	key := os.Getenv("CHOWDAHH_KEY")
	server := &Server{
		key:  key,
		http: &http.Client{Timeout: 30 * time.Second},
	}
	server.serve(os.Stdin, os.Stdout)
}

// --- JSON-RPC framing ---

type rpcRequest struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      json.RawMessage `json:"id,omitempty"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params,omitempty"`
}

type rpcResponse struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      json.RawMessage `json:"id,omitempty"`
	Result  interface{}     `json:"result,omitempty"`
	Error   *rpcError       `json:"error,omitempty"`
}

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

type Server struct {
	key  string
	http *http.Client
}

func (s *Server) serve(in io.Reader, out io.Writer) {
	r := bufio.NewReader(in)
	enc := json.NewEncoder(out)
	for {
		line, err := r.ReadBytes('\n')
		if len(line) > 0 {
			var req rpcRequest
			if jerr := json.Unmarshal(line, &req); jerr == nil {
				resp := s.dispatch(req)
				if resp != nil {
					_ = enc.Encode(resp)
				}
			}
		}
		if err != nil {
			return
		}
	}
}

func (s *Server) dispatch(req rpcRequest) *rpcResponse {
	switch req.Method {
	case "initialize":
		return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{
			"protocolVersion": "2024-11-05",
			"capabilities":    map[string]interface{}{"tools": map[string]interface{}{}},
			"serverInfo":      map[string]interface{}{"name": "chowdahh", "version": "1.0.0"},
		}}
	case "tools/list":
		return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{
			"tools": toolDefinitions(),
		}}
	case "tools/call":
		return s.handleToolCall(req)
	case "notifications/initialized", "notifications/cancelled":
		return nil // notifications expect no response
	default:
		return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32601, Message: "method not found: " + req.Method}}
	}
}

// --- Tools ---

func toolDefinitions() []map[string]interface{} {
	return []map[string]interface{}{
		{
			"name":        "chowdahh_list_streams",
			"description": "List the available curated news stream slugs (top, latest, science, world, tech, business, health, culture, sports, good-news, local).",
			"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
		},
		{
			"name":        "chowdahh_get_stream",
			"description": "Fetch curated news cards from a named Chowdahh stream. Each card is a cluster of corroborating articles, not a single article.",
			"inputSchema": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"slug":   map[string]interface{}{"type": "string", "description": "Stream slug."},
					"limit":  map[string]interface{}{"type": "integer", "default": 10},
					"offset": map[string]interface{}{"type": "integer", "default": 0},
				},
				"required": []string{"slug"},
			},
		},
		{
			"name":        "chowdahh_search",
			"description": "Search Chowdahh clusters by keyword.",
			"inputSchema": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"q":     map[string]interface{}{"type": "string"},
					"limit": map[string]interface{}{"type": "integer", "default": 10},
				},
				"required": []string{"q"},
			},
		},
		{
			"name":        "chowdahh_get_topic",
			"description": "Get a topic's timeline of corroborated facts.",
			"inputSchema": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"topic_id": map[string]interface{}{"type": "string"},
				},
				"required": []string{"topic_id"},
			},
		},
		{
			"name":        "chowdahh_record_signal",
			"description": "Record a reader signal. Types: open, save, share, track, dismiss, source_open, dwell, react, seen, close, tts_play, tts_listen.",
			"inputSchema": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"card_id":     map[string]interface{}{"type": "string"},
					"signal_type": map[string]interface{}{"type": "string"},
				},
				"required": []string{"card_id", "signal_type"},
			},
		},
	}
}

type toolCallParams struct {
	Name      string                 `json:"name"`
	Arguments map[string]interface{} `json:"arguments"`
}

func (s *Server) handleToolCall(req rpcRequest) *rpcResponse {
	var p toolCallParams
	_ = json.Unmarshal(req.Params, &p)

	var body []byte
	var err error
	switch p.Name {
	case "chowdahh_list_streams":
		body, err = s.get("/streams", nil)
	case "chowdahh_get_stream":
		q := url.Values{}
		if v, ok := p.Arguments["limit"]; ok {
			q.Set("limit", fmt.Sprintf("%v", v))
		}
		if v, ok := p.Arguments["offset"]; ok {
			q.Set("offset", fmt.Sprintf("%v", v))
		}
		slug, _ := p.Arguments["slug"].(string)
		body, err = s.get("/streams/"+url.PathEscape(slug), q)
	case "chowdahh_search":
		q := url.Values{}
		if v, ok := p.Arguments["q"].(string); ok {
			q.Set("q", v)
		}
		if v, ok := p.Arguments["limit"]; ok {
			q.Set("limit", fmt.Sprintf("%v", v))
		}
		body, err = s.get("/search", q)
	case "chowdahh_get_topic":
		id, _ := p.Arguments["topic_id"].(string)
		body, err = s.get("/topics/"+url.PathEscape(id), nil)
	case "chowdahh_record_signal":
		payload := []map[string]interface{}{p.Arguments}
		buf, _ := json.Marshal(payload)
		body, err = s.post("/signals", buf)
	default:
		return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: "unknown tool: " + p.Name}}
	}

	if err != nil {
		return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}}
	}
	return &rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{
		"content": []map[string]interface{}{
			{"type": "text", "text": string(redact(body))},
		},
	}}
}

// authHeader is set on every outbound request when the user has supplied a
// key. Bearer form keeps the secret out of URLs / access logs / Referer.
func (s *Server) authHeader(req *http.Request) {
	if s.key != "" {
		req.Header.Set("Authorization", "Bearer "+s.key)
	}
}

func (s *Server) get(path string, q url.Values) ([]byte, error) {
	u := apiBase + path
	if q != nil {
		if encoded := q.Encode(); encoded != "" {
			u += "?" + encoded
		}
	}
	req, _ := http.NewRequest("GET", u, nil)
	req.Header.Set("User-Agent", "chowdahh-mcp/1.0")
	s.authHeader(req)
	return s.do(req)
}

func (s *Server) post(path string, body []byte) ([]byte, error) {
	req, _ := http.NewRequest("POST", apiBase+path, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "chowdahh-mcp/1.0")
	s.authHeader(req)
	return s.do(req)
}

func (s *Server) do(req *http.Request) ([]byte, error) {
	resp, err := s.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	return io.ReadAll(resp.Body)
}
