#!/bin/bash
# todo - aesthetic terminal todo list backed by SQLite
# Inspired by todo.txt-cli. Data lives in ~/.config/scripts/scripts.db
# Usage: todo [command] [args...]

DB="${XDG_CONFIG_HOME:-$HOME/.config}/scripts/scripts.db"

# --- colors ---
C_RESET='\033[0m'
C_BOLD='\033[1m'
C_DIM='\033[2m'
C_ID='\033[33m'         # yellow
C_PRI_A='\033[1;31m'    # bold red
C_PRI_B='\033[1;33m'    # bold yellow
C_PRI_C='\033[1;34m'    # bold blue
C_PRI='\033[1;36m'      # bold cyan (D+)
C_DONE='\033[9;2m'      # strikethrough + dim
C_LIST='\033[35m'       # magenta
C_DATE='\033[2m'        # dim
C_COUNT='\033[1;32m'    # bold green
C_HEADER='\033[1;4m'    # bold underline

# --- init ---
mkdir -p "$(dirname "$DB")"
sqlite3 "$DB" "
CREATE TABLE IF NOT EXISTS todo_lists (
	name TEXT PRIMARY KEY,
	created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
INSERT OR IGNORE INTO todo_lists (name) VALUES ('inbox');
CREATE TABLE IF NOT EXISTS todo (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	task TEXT NOT NULL,
	priority TEXT DEFAULT '',
	list TEXT DEFAULT 'inbox',
	done INTEGER DEFAULT 0,
	created_at TEXT DEFAULT (datetime('now', 'localtime')),
	done_at TEXT DEFAULT ''
);"

# --- helpers ---
die() { printf '%s\n' "$1" >&2; exit 1; }

esc() { printf '%s' "$1" | sed "s/'/''/g"; }

pri_color() {
	case "$1" in
		A) printf '%b' "$C_PRI_A" ;;
		B) printf '%b' "$C_PRI_B" ;;
		C) printf '%b' "$C_PRI_C" ;;
		?*) printf '%b' "$C_PRI" ;;
		*) ;;
	esac
}

fmt_task() {
	id="$1"; task="$2"; pri="$3"; list="$4"; done="$5"; created="$6"
	if [ "$done" = "1" ]; then
		printf "${C_DIM}%4s ${C_DONE}%s${C_RESET}\n" "$id" "$task"
		return
	fi
	pri_str=""
	if [ -n "$pri" ]; then
		pri_str="$(pri_color "$pri")(${pri})${C_RESET} "
	fi
	list_str=""
	if [ "$list" != "inbox" ]; then
		list_str=" ${C_LIST}@${list}${C_RESET}"
	fi
	printf "${C_ID}%4s${C_RESET} ${pri_str}%s${list_str}\n" "$id" "$task"
}

# --- commands ---

cmd_add() {
	[ -z "$1" ] && die "Usage: todo add \"task text\" [-p PRI] [-l LIST]"
	task=""; pri=""; list="inbox"
	while [ $# -gt 0 ]; do
		case "$1" in
			-p) shift; pri=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]'); shift ;;
			-l) shift; list="$1"; shift ;;
			*) [ -n "$task" ] && task="$task $1" || task="$1"; shift ;;
		esac
	done
	[ -z "$task" ] && die "No task text provided"
	sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")');"
	new_id=$(sqlite3 "$DB" "INSERT INTO todo (task, priority, list) VALUES ('$(esc "$task")', '$(esc "$pri")', '$(esc "$list")'); SELECT last_insert_rowid();")
	printf "${C_COUNT}+${C_RESET} %s ${C_DIM}#%s${C_RESET}\n" "$task" "$new_id"
}

cmd_list() {
	filter_list=""; search=""; show_done=0; show_all=0
	while [ $# -gt 0 ]; do
		case "$1" in
			-l) shift; filter_list="$1"; shift ;;
			-a) show_all=1; shift ;;
			--done) show_done=1; shift ;;
			*) search="$1"; shift ;;
		esac
	done

	where="1=1"
	if [ "$show_all" = "0" ] && [ "$show_done" = "0" ]; then
		where="done = 0"
	elif [ "$show_done" = "1" ]; then
		where="done = 1"
	fi
	[ -n "$filter_list" ] && where="$where AND list = '$(esc "$filter_list")'"
	[ -n "$search" ] && where="$where AND task LIKE '%$(esc "$search")%'"

	order="CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id"

	result=$(sqlite3 -separator '|' "$DB" "SELECT id, task, priority, list, done, created_at FROM todo WHERE $where ORDER BY $order;")

	if [ -z "$result" ]; then
		printf "${C_DIM}No tasks found${C_RESET}\n"
		return
	fi

	printf '%s\n' "$result" | while IFS='|' read -r id task pri list done created; do
		fmt_task "$id" "$task" "$pri" "$list" "$done" "$created"
	done

	total=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 0;")
	done_count=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	printf "\n${C_DIM}─── ${C_COUNT}%s${C_DIM} open · %s done ───${C_RESET}\n" "$total" "$done_count"
}

cmd_do() {
	[ -z "$1" ] && die "Usage: todo do ID [ID ...]"
	for id in "$@"; do
		task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id AND done = 0;")
		[ -z "$task" ] && { printf "${C_DIM}#%s not found or already done${C_RESET}\n" "$id"; continue; }
		sqlite3 "$DB" "UPDATE todo SET done = 1, done_at = datetime('now', 'localtime') WHERE id = $id;"
		printf "${C_DONE}✓${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$id" "$task"
	done
}

cmd_undo() {
	[ -z "$1" ] && die "Usage: todo undo ID"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $1 AND done = 1;")
	[ -z "$task" ] && die "#$1 not found or not done"
	sqlite3 "$DB" "UPDATE todo SET done = 0, done_at = '' WHERE id = $1;"
	printf "${C_COUNT}↩${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$1" "$task"
}

cmd_rm() {
	[ -z "$1" ] && die "Usage: todo rm ID [ID ...]"
	for id in "$@"; do
		task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
		[ -z "$task" ] && { printf "${C_DIM}#%s not found${C_RESET}\n" "$id"; continue; }
		sqlite3 "$DB" "DELETE FROM todo WHERE id = $id;"
		printf "${C_DIM}× #%s %s${C_RESET}\n" "$id" "$task"
	done
}

cmd_pri() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo pri ID PRIORITY"
	id="$1"
	pri=$(printf '%s' "$2" | tr '[:lower:]' '[:upper:]')
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	[ -z "$task" ] && die "#$id not found"
	sqlite3 "$DB" "UPDATE todo SET priority = '$(esc "$pri")' WHERE id = $id;"
	printf "$(pri_color "$pri")(%s)${C_RESET} ${C_DIM}#%s${C_RESET} %s\n" "$pri" "$id" "$task"
}

cmd_depri() {
	[ -z "$1" ] && die "Usage: todo depri ID"
	sqlite3 "$DB" "UPDATE todo SET priority = '' WHERE id = $1;"
	printf "${C_DIM}Removed priority from #%s${C_RESET}\n" "$1"
}

cmd_edit() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo edit ID \"new task text\""
	id="$1"; shift
	new_task="$*"
	sqlite3 "$DB" "UPDATE todo SET task = '$(esc "$new_task")' WHERE id = $id;"
	printf "${C_DIM}Edited #%s →${C_RESET} %s\n" "$id" "$new_task"
}

cmd_mv() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo mv ID LIST"
	id="$1"; list="$2"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	[ -z "$task" ] && die "#$id not found"
	sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")'); UPDATE todo SET list = '$(esc "$list")' WHERE id = $id;"
	printf "${C_DIM}#%s →${C_RESET} ${C_LIST}@%s${C_RESET} %s\n" "$id" "$list" "$task"
}

cmd_lists() {
	printf "${C_HEADER}Lists${C_RESET}\n"
	sqlite3 -separator '|' "$DB" "
		SELECT l.name,
			COALESCE(SUM(CASE WHEN t.done = 0 THEN 1 ELSE 0 END), 0) as open,
			COALESCE(SUM(CASE WHEN t.done = 1 THEN 1 ELSE 0 END), 0) as done
		FROM todo_lists l
		LEFT JOIN todo t ON t.list = l.name
		GROUP BY l.name ORDER BY open DESC, l.name;
	" | while IFS='|' read -r list open done_n; do
		printf "  ${C_LIST}@%-15s${C_RESET} ${C_COUNT}%3s${C_RESET} open  ${C_DIM}%3s done${C_RESET}\n" "$list" "$open" "$done_n"
	done
}

cmd_mklist() {
	[ -z "$1" ] && die "Usage: todo mklist NAME"
	name="$1"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$name")';")
	[ "$exists" != "0" ] && die "@$name already exists"
	sqlite3 "$DB" "INSERT INTO todo_lists (name) VALUES ('$(esc "$name")');"
	printf "${C_COUNT}+${C_RESET} ${C_LIST}@%s${C_RESET} created\n" "$name"
}

cmd_rmlist() {
	[ -z "$1" ] && die "Usage: todo rmlist NAME"
	name="$1"
	[ "$name" = "inbox" ] && die "Cannot delete @inbox"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$name")';")
	[ "$exists" = "0" ] && die "@$name not found"
	count=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE list = '$(esc "$name")' AND done = 0;")
	if [ "$count" != "0" ]; then
		printf "${C_DIM}@%s has %s open tasks. Move them to inbox? [y/N]${C_RESET} " "$name" "$count"
		read -r ans
		case "$ans" in
			y|Y) sqlite3 "$DB" "UPDATE todo SET list = 'inbox' WHERE list = '$(esc "$name")';" ;;
			*) die "Aborted" ;;
		esac
	fi
	sqlite3 "$DB" "DELETE FROM todo WHERE list = '$(esc "$name")'; DELETE FROM todo_lists WHERE name = '$(esc "$name")';"
	printf "${C_DIM}× @%s deleted${C_RESET}\n" "$name"
}

cmd_renlist() {
	[ -z "$1" ] || [ -z "$2" ] && die "Usage: todo renlist OLD NEW"
	old="$1"; new="$2"
	[ "$old" = "inbox" ] && die "Cannot rename @inbox"
	exists=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$old")';")
	[ "$exists" = "0" ] && die "@$old not found"
	taken=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo_lists WHERE name = '$(esc "$new")';")
	[ "$taken" != "0" ] && die "@$new already exists"
	sqlite3 "$DB" "
		UPDATE todo_lists SET name = '$(esc "$new")' WHERE name = '$(esc "$old")';
		UPDATE todo SET list = '$(esc "$new")' WHERE list = '$(esc "$old")';
	"
	printf "${C_LIST}@%s${C_RESET} ${C_DIM}→${C_RESET} ${C_LIST}@%s${C_RESET}\n" "$old" "$new"
}

cmd_search() {
	[ -z "$1" ] && die "Usage: todo search TERM"
	cmd_list "$1"
}

cmd_archive() {
	n=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	[ "$n" = "0" ] && { printf "${C_DIM}Nothing to archive${C_RESET}\n"; return; }
	sqlite3 "$DB" "DELETE FROM todo WHERE done = 1;"
	printf "${C_DIM}Archived %s completed tasks${C_RESET}\n" "$n"
}

cmd_count() {
	open=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 0;")
	done_n=$(sqlite3 "$DB" "SELECT COUNT(*) FROM todo WHERE done = 1;")
	total=$((open + done_n))
	printf "${C_COUNT}%s${C_RESET} open  ${C_DIM}%s done  %s total${C_RESET}\n" "$open" "$done_n" "$total"
}

# --- dmenu modes ---

cmd_dmenu_add() {
	# pick list
	lists=$(sqlite3 "$DB" "SELECT name FROM todo_lists ORDER BY name;")
	[ -z "$lists" ] && lists="inbox"
	list=$(printf '%s\nNew list...' "$lists" | dmenu -i -l 10 -p "List:" || exit 0)
	[ -z "$list" ] && exit 0

	if [ "$list" = "New list..." ]; then
		list=$(printf '' | dmenu -l 1 -p "List name:" || exit 0)
		[ -z "$list" ] && exit 0
		sqlite3 "$DB" "INSERT OR IGNORE INTO todo_lists (name) VALUES ('$(esc "$list")');"
	fi

	# pick priority
	pri=$(printf 'None\nA\nB\nC\nD' | dmenu -i -l 5 -p "Priority:" || exit 0)
	[ -z "$pri" ] && exit 0
	[ "$pri" = "None" ] && pri=""

	# task text
	task=$(printf '' | dmenu -c -l 1 -h 40 -p "Todo:" || exit 0)
	[ -z "$task" ] && exit 0

	sqlite3 "$DB" "INSERT INTO todo (task, priority, list) VALUES ('$(esc "$task")', '$(esc "$pri")', '$(esc "$list")');"
	notify-send "Todo added" "$task → @$list"
}

cmd_dmenu_view() {
	# pick a list
	lists=$(sqlite3 "$DB" "SELECT list || ' (' || SUM(CASE WHEN done=0 THEN 1 ELSE 0 END) || ')' FROM todo GROUP BY list ORDER BY list;")
	[ -z "$lists" ] && { notify-send "Todo" "No lists found"; exit 0; }

	selected=$(printf '%s' "$lists" | dmenu -i -l 15 -p "View list:" || exit 0)
	[ -z "$selected" ] && exit 0

	list=$(printf '%s' "$selected" | sed 's/ ([0-9]*)$//')

	tasks=$(sqlite3 -separator '|' "$DB" "SELECT priority, task FROM todo WHERE list = '$(esc "$list")' AND done = 0 ORDER BY CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id;")

	if [ -z "$tasks" ]; then
		notify-send "@$list" "No open tasks"
		exit 0
	fi

	# format for dunst
	body=""
	n=0
	printf '%s\n' "$tasks" | while IFS='|' read -r pri task; do
		n=$((n + 1))
		if [ -n "$pri" ]; then
			line="($pri) $task"
		else
			line="    $task"
		fi
		if [ -n "$body" ]; then
			body="$body
$line"
		else
			body="$line"
		fi
		# dunst gets it all at the end
		printf '%s\n' "$line"
	done | {
		body=$(cat)
		count=$(printf '%s\n' "$body" | wc -l)
		notify-send -t 10000 "@$list  [$count]" "$body"
	}
}

cmd_dmenu_do() {
	tasks=$(sqlite3 -separator ' | ' "$DB" "SELECT id, CASE WHEN priority != '' THEN '(' || priority || ') ' ELSE '' END || task || ' @' || list FROM todo WHERE done = 0 ORDER BY CASE WHEN priority = '' THEN 1 ELSE 0 END, priority, id;")
	[ -z "$tasks" ] && { notify-send "Todo" "No open tasks"; exit 0; }

	selected=$(printf '%s' "$tasks" | dmenu -i -l 20 -p "Complete:" || exit 0)
	[ -z "$selected" ] && exit 0

	id=$(printf '%s' "$selected" | cut -d'|' -f1 | tr -d ' ')
	sqlite3 "$DB" "UPDATE todo SET done = 1, done_at = datetime('now', 'localtime') WHERE id = $id;"
	task=$(sqlite3 "$DB" "SELECT task FROM todo WHERE id = $id;")
	notify-send "Done" "$task"
}

# --- main ---

cmd_help() {
	printf '%b' "\
${C_BOLD}todo${C_RESET} - terminal todo list

${C_HEADER}Tasks${C_RESET}
  todo add \"task\" [-p PRI] [-l LIST]    Add a task
  todo ls [-l LIST] [-a] [--done] [Q]   List tasks
  todo do ID [ID ...]                   Mark task(s) done
  todo undo ID                          Reopen a task
  todo rm ID [ID ...]                   Delete task(s)
  todo pri ID A-Z                       Set priority
  todo depri ID                         Remove priority
  todo edit ID \"new text\"               Edit task text
  todo mv ID LIST                       Move task to a list
  todo search TERM                      Search tasks
  todo archive                          Purge completed tasks
  todo count                            Task counts

${C_HEADER}Lists${C_RESET}
  todo lists                            Show all lists
  todo mklist NAME                      Create a new list
  todo rmlist NAME                      Delete a list
  todo renlist OLD NEW                  Rename a list

${C_HEADER}Dmenu${C_RESET}
  todo -d                               Dmenu add mode
  todo -D                               Dmenu view list → dunst
  todo -x                               Dmenu complete mode

${C_HEADER}Notes${C_RESET}
  Default list is ${C_LIST}@inbox${C_RESET}. Use ${C_DIM}-l${C_RESET} to filter or add to a list.
  Priorities: ${C_PRI_A}(A)${C_RESET} ${C_PRI_B}(B)${C_RESET} ${C_PRI_C}(C)${C_RESET} highest → lowest.
"
}

case "$1" in
	add|a)       shift; cmd_add "$@" ;;
	ls|list)     shift; cmd_list "$@" ;;
	do|done)     shift; cmd_do "$@" ;;
	undo)        shift; cmd_undo "$@" ;;
	rm|del)      shift; cmd_rm "$@" ;;
	pri|p)       shift; cmd_pri "$@" ;;
	depri|dp)    shift; cmd_depri "$@" ;;
	edit)        shift; cmd_edit "$@" ;;
	mv|move)     shift; cmd_mv "$@" ;;
	lists)       cmd_lists ;;
	mklist)      shift; cmd_mklist "$@" ;;
	rmlist)      shift; cmd_rmlist "$@" ;;
	renlist)     shift; cmd_renlist "$@" ;;
	search|s)    shift; cmd_search "$@" ;;
	archive)     cmd_archive ;;
	count)       cmd_count ;;
	-d|--dmenu)  cmd_dmenu_add ;;
	-D|--dview)  cmd_dmenu_view ;;
	-x|--ddo)    cmd_dmenu_do ;;
	-h|--help|help) cmd_help ;;
	"")          cmd_list ;;
	*)           printf "Unknown command: %s (try todo -h)\n" "$1" ;;
esac
