package cmd

import (
	"axolotl/models"
	"axolotl/service"
	"axolotl/store"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"sort"
	"strings"
	"time"

	"github.com/fatih/color"
)

type RenderMap map[string]struct {
	s string
	l string
	c *color.Color
}

var (
	cPrimary = color.New(color.FgCyan)
	cSecond  = color.New(color.FgMagenta)
	cDim     = color.New(color.FgHiBlack)
	cText    = color.New(color.FgWhite)
	cTitle   = color.New(color.FgWhite, color.Bold)
	cGood    = color.New(color.FgGreen)
	cWarn    = color.New(color.FgYellow)
	cBad     = color.New(color.FgRed)

	typeRM = RenderMap{
		"issue":     {" ", "\uf188 issue", cSecond},
		"note":      {"\uf15c", "\uf15c note", cPrimary},
		"user":      {"\uf007", "\uf007 user", cGood},
		"namespace": {"\uf07b", "\uf07b namespace", cWarn},
		"":          {" ", "n/a", cDim},
	}
	statusRM = RenderMap{
		"open": {"●", "● open", cPrimary},
		"done": {"○", "○ done", cDim},
		"":     {"—", "n/a", cDim},
	}
	prioRM = RenderMap{
		"high":   {"\uf0e7", "high", cBad},
		"medium": {"\uf0e7", "medium", cWarn},
		"low":    {" ", "low", cDim},
		"":       {" ", "n/a", cDim},
	}
	relIcons = map[string]string{"blocks": "\uf068", "subtask": "\uf0da", "related": "\uf0c1", "assignee": "\uf007"}
	prioRanks   = map[string]int{"high": 3, "medium": 2, "low": 1}
	statusRanks = map[string]int{"open": 2, "": 1, "done": 0}
)

const (
	iconCalendar  = "\uf133"
	iconCheck     = "\uf00c"
	iconCross     = "\uf00d"
	iconNamespace = "\uf07b"
)

func PrintNodes(w io.Writer, svc service.NodeService, nodes []*models.Node, jsonOut bool) error {
	if jsonOut {
		return json.NewEncoder(w).Encode(nodes)
	}
	if len(nodes) == 0 {
		fmt.Fprintln(w, cDim.Sprint("No results."))
		return nil
	}

	fmt.Fprintln(w)
	sort.Slice(nodes, func(i, j int) bool {
		si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status")
		if si != sj {
			return statusRanks[si] > statusRanks[sj]
		}
		pi, pj := prioRanks[nodes[i].GetProperty("prio")], prioRanks[nodes[j].GetProperty("prio")]
		if pi != pj {
			return pi > pj
		}
		di, dj := nodes[i].DueDate, nodes[j].DueDate
		if di == nil && dj == nil {
			return false
		}
		if di == nil {
			return false
		}
		if dj == nil {
			return true
		}
		return di.Before(dj.Time) // soonest due date first
	})

	for _, n := range nodes {
		fmt.Fprintf(w, "  %s %s %s %s %s %s",
			cDim.Sprint(n.ID),
			render(prioRM, n.GetProperty("prio"), true),
			render(statusRM, n.GetProperty("status"), true),
			render(typeRM, n.GetProperty("type"), true),
			cTitle.Sprint(truncate(n.Title, 80)),
			dueDateShort(n.DueDate),
		)
		tags := n.GetDisplayTags()
		if len(tags) > 0 {
			fmt.Fprintf(w, " %s", cPrimary.Sprint("#"+strings.Join(tags, " #")))
		}
		fmt.Fprintln(w)
	}
	fmt.Fprintln(w)
	return nil
}

func PrintNode(w io.Writer, svc service.NodeService, n *models.Node, jsonOut bool) error {
	if jsonOut {
		return json.NewEncoder(w).Encode(n)
	}
	fmt.Fprintln(w)
	fmt.Fprintf(w, "  %s %s %s\n", render(typeRM, n.GetProperty("type"), false), cDim.Sprint(n.ID), cTitle.Sprint(n.Title))
	fmt.Fprintln(w, cDim.Sprint("  ───────────────────────────────"))
	fmt.Fprintf(w, "  Status:    %s\n", render(statusRM, n.GetProperty("status"), false))
	fmt.Fprintf(w, "  Priority:  %s\n", render(prioRM, n.GetProperty("prio"), false))
	if n.DueDate != nil {
		fmt.Fprintf(w, "  Due:       %s\n", dueDateLong(n.DueDate))
	}
	fmt.Fprintf(w, "  Created:   %s\n", cDim.Sprint(n.CreatedAt))
	fmt.Fprintf(w, "  Updated:   %s\n", cDim.Sprint(n.UpdatedAt))

	if tags := n.GetDisplayTags(); len(tags) > 0 {
		fmt.Fprintf(w, "\n  tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • ")))
	}

	n_rels := n.Relations
	for relType := range n_rels {
		rel_node_ids := n_rels[string(relType)]
		if len(rel_node_ids) > 0 {
			fmt.Fprintf(w, "\n  %s\n", string(relType))
		}
		for _, id := range rel_node_ids {
			rel_node, err := svc.GetByID(id)
			if err != nil {
				fmt.Fprintf(w, "    %s %s\n", relIcons[relType], cDim.Sprint(id))
				continue
			}
			fmt.Fprintf(w, "    %s %s\n", relIcons[relType], rel_node.Title)
		}
	}

	if n.Content != "" {
		fmt.Fprintln(w, "\n"+cPrimary.Sprint("  Content:"))
		for i, line := range strings.Split(n.Content, "\n") {
			if i > 5 {
				fmt.Fprintf(w, "%s ...\n", cDim.Sprint("  │ "))
				break
			}
			fmt.Fprintf(w, "%s%s\n", cDim.Sprint("  │ "), cText.Sprint(line))
		}
	}

	fmt.Fprintln(w)
	return nil
}

func PrintAliases(w io.Writer, aliases []*store.Alias, jsonOut bool) error {
	if jsonOut {
		return json.NewEncoder(w).Encode(aliases)
	}
	if len(aliases) == 0 {
		fmt.Fprintln(w, cDim.Sprint("No aliases defined."))
		return nil
	}
	fmt.Fprintln(w)
	for _, a := range aliases {
		fmt.Fprintf(w, "  %s  %s\n", cPrimary.Sprint(a.Name), cDim.Sprint(a.Command))
		if a.Description != "" {
			fmt.Fprintf(w, "    %s\n", cDim.Sprint(a.Description))
		}
	}
	fmt.Fprintln(w)
	return nil
}

func PrintAction(w io.Writer, action, detail string, isError bool) {
	if isError {
		fmt.Fprintln(w, cBad.Sprint(iconCross+" "+action+" ")+cDim.Sprint(detail))
		return
	}
	icon := iconCheck
	if action == "Created" {
		icon = iconNamespace
	}
	fmt.Fprintln(w, cGood.Sprint(icon+" "+action+" ")+cDim.Sprint(detail))
}

func render(rm RenderMap, key string, short bool) string {
	v, ok := rm[key]
	if !ok {
		v, ok = rm[""]
		if !ok {
			return ""
		}
	}
	if short {
		return v.c.Sprint(v.s)
	}
	return v.c.Sprint(v.l)
}

// dueDateShort returns a "+12d" / "-3d" style string colored by urgency.
func dueDateShort(d *models.Date) string {
	if d == nil {
		return ""
	}
	now := time.Now().UTC().Truncate(24 * time.Hour)
	due := d.UTC().Truncate(24 * time.Hour)
	days := int(math.Round(due.Sub(now).Hours() / 24))

	var label string
	if days == 0 {
		label = "today"
	} else if days > 0 {
		label = fmt.Sprintf("+%dd", days)
	} else {
		label = fmt.Sprintf("%dd", days)
	}

	var c *color.Color
	switch {
	case days < 0:
		c = cBad
	case days <= 3:
		c = cWarn
	default:
		c = cGood
	}
	return c.Sprint(label)
}

// dueDateLong returns the full date string colored by urgency.
func dueDateLong(d *models.Date) string {
	if d == nil {
		return ""
	}
	now := time.Now().UTC().Truncate(24 * time.Hour)
	due := d.UTC().Truncate(24 * time.Hour)
	days := int(math.Round(due.Sub(now).Hours() / 24))

	var c *color.Color
	switch {
	case days < 0:
		c = cBad
	case days <= 3:
		c = cWarn
	default:
		c = cGood
	}
	return c.Sprintf("%s %s", iconCalendar, d.Format("2006-01-02"))
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max-1] + "…"
}
