package main

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

// ToolResult is the uniform shape returned to the chat loop after a tool call.
type ToolResult struct {
	OK     bool   `json:"ok"`
	Result any    `json:"result,omitempty"`
	Error  string `json:"error,omitempty"`
}

func okResult(v any) ToolResult       { return ToolResult{OK: true, Result: v} }
func errResult(err error) ToolResult  { return ToolResult{OK: false, Error: err.Error()} }
func errMsg(msg string) ToolResult    { return ToolResult{OK: false, Error: msg} }

// executeTool dispatches a tool by name with raw JSON input and returns a ToolResult.
// Used by the legacy chat path (no authenticated user available).
func executeTool(db *DB, name string, input json.RawMessage) ToolResult {
	return executeToolAs(db, name, input, "")
}

// executeToolAs is the actor-aware dispatch used by the HTTP MCP path.
// actor is the authenticated user id (resolved from the bearer token) for tools
// that need it (add_comment / delete_comment infer the author from it).
func executeToolAs(db *DB, name string, input json.RawMessage, actor string) ToolResult {
	switch name {
	case "list_board":
		return toolListBoard(db)
	case "create_column":
		return toolCreateColumn(db, input)
	case "update_column":
		return toolUpdateColumn(db, input)
	case "rename_column": // alias for backwards compat
		return toolUpdateColumn(db, input)
	case "delete_column":
		return toolDeleteColumn(db, input)
	case "reorder_columns":
		return toolReorderColumns(db, input)
	case "create_card":
		return toolCreateCard(db, input)
	case "update_card":
		return toolUpdateCard(db, input)
	case "delete_card":
		return toolDeleteCard(db, input)
	case "move_card":
		return toolMoveCard(db, input)
	case "card_history":
		return toolCardHistory(db, input)
	case "find_cards":
		return toolFindCards(db, input)
	case "list_users":
		return toolListUsers(db)
	case "assign_card":
		return toolAssignCard(db, input)
	case "get_card":
		return toolGetCard(db, input)
	case "add_comment":
		return toolAddCommentAs(db, input, actor)
	case "list_comments":
		return toolListComments(db, input)
	case "delete_comment":
		return toolDeleteComment(db, input, actor)
	default:
		return errMsg("unknown tool: " + name)
	}
}

// toolMutates reports whether a successful invocation modifies the board state.
func toolMutates(name string) bool {
	switch name {
	case "create_column", "update_column", "rename_column", "delete_column", "reorder_columns",
		"create_card", "update_card", "delete_card", "move_card", "assign_card",
		"add_comment", "delete_comment":
		return true
	}
	return false
}

func toolListBoard(db *DB) ToolResult {
	cols, err := db.ListColumns()
	if err != nil {
		return errResult(err)
	}
	cards, err := db.ListCardsWithTime()
	if err != nil {
		return errResult(err)
	}
	return okResult(map[string]any{"columns": cols, "cards": cards})
}

func toolCreateColumn(db *DB, input json.RawMessage) ToolResult {
	var in struct{ Name string `json:"name"` }
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if strings.TrimSpace(in.Name) == "" {
		return errMsg("name required")
	}
	c, err := db.CreateColumn(in.Name)
	if err != nil {
		return errResult(err)
	}
	return okResult(c)
}

func toolUpdateColumn(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		ID       string  `json:"id"`
		Name     *string `json:"name"`
		Location *string `json:"location"`
		Width    *int    `json:"width"`
		WIPLimit *int    `json:"wip_limit"`
		IsDone   *bool   `json:"is_done"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	if in.Name == nil && in.Location == nil && in.Width == nil && in.WIPLimit == nil && in.IsDone == nil {
		return errMsg("at least one of name/location/width/wip_limit/is_done required")
	}
	if err := db.UpdateColumn(in.ID, ColumnPatch{Name: in.Name, Location: in.Location, Width: in.Width, WIPLimit: in.WIPLimit, IsDone: in.IsDone}); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolDeleteColumn(db *DB, input json.RawMessage) ToolResult {
	var in struct{ ID string }
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	if err := db.DeleteColumn(in.ID); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolReorderColumns(db *DB, input json.RawMessage) ToolResult {
	var in struct{ IDs []string }
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if len(in.IDs) == 0 {
		return errMsg("ids required")
	}
	if err := db.ReorderColumns(in.IDs); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolCreateCard(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		ColumnID    string `json:"column_id"`
		Requester   string `json:"requester"`
		Title       string `json:"title"`
		Description string `json:"description"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ColumnID == "" || strings.TrimSpace(in.Title) == "" {
		return errMsg("column_id and title required")
	}
	c, err := db.CreateCard(in.ColumnID, in.Requester, in.Title, in.Description, "")
	if err != nil {
		return errResult(err)
	}
	return okResult(c)
}

func toolUpdateCard(db *DB, input json.RawMessage) ToolResult {
	var raw map[string]any
	if err := json.Unmarshal(input, &raw); err != nil {
		return errResult(err)
	}
	id, _ := raw["id"].(string)
	if id == "" {
		return errMsg("id required")
	}
	patch := CardPatch{}
	if v, ok := raw["requester"].(string); ok {
		patch.Requester = &v
	}
	if v, ok := raw["title"].(string); ok {
		patch.Title = &v
	}
	if v, ok := raw["description"].(string); ok {
		patch.Description = &v
	}
	if v, ok := raw["color"].(string); ok {
		patch.Color = &v
	}
	if v, ok := raw["locked"].(bool); ok {
		patch.Locked = &v
	}
	if v, present := raw["assignee_id"]; present {
		patch.HasAssignee = true
		if v == nil {
			empty := ""
			patch.AssigneeID = &empty
		} else if s, ok := v.(string); ok {
			patch.AssigneeID = &s
		}
	}
	if err := db.UpdateCard(id, patch); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolListUsers(db *DB) ToolResult {
	users, err := db.ListUsers()
	if err != nil {
		return errResult(err)
	}
	return okResult(users)
}

func toolAssignCard(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		ID         string  `json:"id"`
		AssigneeID *string `json:"assignee_id"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	patch := CardPatch{HasAssignee: true}
	if in.AssigneeID == nil {
		empty := ""
		patch.AssigneeID = &empty
	} else {
		patch.AssigneeID = in.AssigneeID
	}
	if err := db.UpdateCard(in.ID, patch); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolDeleteCard(db *DB, input json.RawMessage) ToolResult {
	var in struct{ ID string }
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	if err := db.DeleteCard(in.ID); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

// toolMoveCard accepts {id, column_id, ordered_ids?}. If ordered_ids is missing,
// the card is appended to the end of the destination column.
func toolMoveCard(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		ID         string   `json:"id"`
		ColumnID   string   `json:"column_id"`
		OrderedIDs []string `json:"ordered_ids"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" || in.ColumnID == "" {
		return errMsg("id and column_id required")
	}
	if len(in.OrderedIDs) == 0 {
		cards, err := db.ListCardsWithTime()
		if err != nil {
			return errResult(err)
		}
		var dest []Card
		for _, c := range cards {
			if c.ColumnID == in.ColumnID && c.ID != in.ID {
				dest = append(dest, c)
			}
		}
		sort.Slice(dest, func(i, j int) bool { return dest[i].Position < dest[j].Position })
		ids := make([]string, 0, len(dest)+1)
		for _, c := range dest {
			ids = append(ids, c.ID)
		}
		ids = append(ids, in.ID)
		in.OrderedIDs = ids
	}
	if err := db.MoveCard(in.ID, in.ColumnID, in.OrderedIDs, ""); err != nil {
		return errResult(err)
	}
	return okResult(nil)
}

func toolCardHistory(db *DB, input json.RawMessage) ToolResult {
	var in struct{ ID string }
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	hist, err := db.CardHistory(in.ID)
	if err != nil {
		return errResult(err)
	}
	return okResult(hist)
}

func toolFindCards(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		Query     string `json:"query"`
		ColumnID  string `json:"column_id"`
		Requester string `json:"requester"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	cards, err := db.ListCardsWithTime()
	if err != nil {
		return errResult(err)
	}
	q := strings.ToLower(strings.TrimSpace(in.Query))
	col := in.ColumnID
	req := strings.ToLower(strings.TrimSpace(in.Requester))
	out := make([]Card, 0, len(cards))
	for _, c := range cards {
		if col != "" && c.ColumnID != col {
			continue
		}
		if req != "" && !strings.Contains(strings.ToLower(c.Requester), req) {
			continue
		}
		if q != "" {
			hay := strings.ToLower(c.Title + " " + c.Description + " " + c.Requester)
			if !strings.Contains(hay, q) {
				continue
			}
		}
		out = append(out, c)
	}
	return okResult(out)
}

// validateToolName fails fast with clearer error than the dispatch's default.
func validateToolName(name string) error {
	known := map[string]bool{
		"list_board": true, "create_column": true, "update_column": true, "rename_column": true,
		"delete_column": true, "reorder_columns": true, "create_card": true,
		"update_card": true, "delete_card": true, "move_card": true,
		"card_history": true, "find_cards": true,
		"list_users": true, "assign_card": true,
		"add_comment": true, "list_comments": true, "delete_comment": true,
		"get_card": true,
	}
	if !known[name] {
		return fmt.Errorf("unknown tool: %s", name)
	}
	return nil
}

// toolAddCommentAs appends a comment (card_message) to a card.
//
// Author resolution order:
//  1. explicit "author_id" in input (legacy chat path)
//  2. explicit "author_username" in input -> resolve to id
//  3. fallback to `actor` (authenticated user from MCP HTTP token)
//
// At least one must yield a non-empty id.
func toolAddCommentAs(db *DB, input json.RawMessage, actor string) ToolResult {
	var in struct {
		CardID         string `json:"card_id"`
		Body           string `json:"body"`
		AuthorID       string `json:"author_id"`
		AuthorUsername string `json:"author_username"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.CardID == "" {
		return errMsg("card_id required")
	}
	if strings.TrimSpace(in.Body) == "" {
		return errMsg("body required")
	}
	authorID := strings.TrimSpace(in.AuthorID)
	if authorID == "" && in.AuthorUsername != "" {
		u, _, err := db.GetUserByUsername(in.AuthorUsername)
		if err != nil {
			return errResult(fmt.Errorf("author_username: %w", err))
		}
		authorID = u.ID
	}
	if authorID == "" {
		authorID = actor
	}
	if authorID == "" {
		return errMsg("author_id, author_username, or authenticated MCP token required")
	}
	m, err := db.CreateCardMessage(in.CardID, authorID, in.Body)
	if err != nil {
		return errResult(err)
	}
	return okResult(m)
}

// toolGetCard returns a single active (non-archived) card by id or seq_num.
// Pass exactly ONE of {id, seq_num}.
func toolGetCard(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		ID     string `json:"id"`
		SeqNum int    `json:"seq_num"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" && in.SeqNum == 0 {
		return errMsg("provide id or seq_num")
	}
	cards, err := db.ListCardsWithTime()
	if err != nil {
		return errResult(err)
	}
	for _, c := range cards {
		if in.ID != "" && c.ID == in.ID {
			return okResult(c)
		}
		if in.SeqNum != 0 && c.SeqNum == in.SeqNum {
			return okResult(c)
		}
	}
	return errMsg("card not found")
}

// toolDeleteComment deletes a comment. Only the original author can delete it
// (enforced via actor == message.author_id).
func toolDeleteComment(db *DB, input json.RawMessage, actor string) ToolResult {
	if actor == "" {
		return errMsg("authenticated user required (call via MCP HTTP with a valid token)")
	}
	var in struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.ID == "" {
		return errMsg("id required")
	}
	if err := db.DeleteCardMessage(in.ID, actor); err != nil {
		return errResult(err)
	}
	return okResult(map[string]bool{"ok": true})
}

// toolListComments returns every comment (card_message) attached to a card
// sorted by created_at ascending.
func toolListComments(db *DB, input json.RawMessage) ToolResult {
	var in struct {
		CardID string `json:"card_id"`
	}
	if err := json.Unmarshal(input, &in); err != nil {
		return errResult(err)
	}
	if in.CardID == "" {
		return errMsg("card_id required")
	}
	msgs, err := db.ListCardMessages(in.CardID)
	if err != nil {
		return errResult(err)
	}
	return okResult(msgs)
}
