package main

import "core:flags"
import "core:fmt"
import "core:strconv"
import "core:strings"
import "core:time"

import "../core"

Command :: enum {
	Usage,
	Add,
	Symptom,
	Event,
	Today,
	Week,
	Show,
	Coffee,
	Events,
	Version,
}

Command_Func :: distinct proc(args: []string, store: ^core.Storage)

Command_Data := [Command]struct {
	func: Command_Func,
	desc: string,
} {
	.Usage = {func = command_usage, desc = "this screen"},
	.Add = {func = command_add, desc = "log today's daily entry"},
	.Symptom = {func = command_symptom, desc = "note a symptom"},
	.Coffee = {func = command_coffee, desc = "alias to note a cup of coffee"},
	.Event = {func = command_event, desc = "log a timestamped event"},
	.Today = {func = command_today, desc = "show today"},
	.Week = {func = command_week, desc = "show current week"},
	.Show = {func = command_show, desc = "show a specific date"},
	.Events = {func = command_events, desc = "list events"},
	.Version = {func = command_version, desc = "print version"},
}

command_from_string :: proc(cmd_str: string) -> Command {
	cmd: Command
	switch cmd_str {
	case "usage":
		cmd = .Usage
	case "add":
		cmd = .Add
	case "symptom":
		cmd = .Symptom
	case "coffee":
		cmd = .Coffee
	case "event":
		cmd = .Event
	case "today":
		cmd = .Today
	case "week":
		cmd = .Week
	case "show":
		cmd = .Show
	case "events":
		cmd = .Events
	case "version":
		cmd = .Version
	}
	return cmd
}

command_usage :: proc(args: []string, store: ^core.Storage) {
	fmt.printf("%s🧌 Data Goblin%s v%s\n\n", core.BOLD, core.RESET, "0.1")
	fmt.printf("  %shoarding tiny truths since 2026%s\n", core.DIM, core.RESET)
	fmt.printf("  %sThe goblin stares at you. You forgot to say what you want.%s\n\n", core.DIM, core.RESET)
	fmt.printf("  Available commands:\n\n")
	for data, cmd in Command_Data {
		fmt.printf(
			"    %s%-14s%s %s%s%s\n",
			core.GREEN,
			strings.to_lower(fmt.tprintf("%s", cmd), context.temp_allocator),
			core.RESET,
			core.DIM,
			data.desc,
			core.RESET,
		)
	}
	fmt.printf("\n  Usage: %sdgob  [flags]%s\n\n", core.YELLOW, core.RESET)
}

Init_Options :: struct {
	db: string `usage:"path to sqlite-file to use (defaults to ./data.db)"`,
}

validate_ranges :: proc(model: rawptr, name: string, value: any, args_tag: string) -> string {
	switch name {
	case "mood", "sleep_quality":
		if n, ok := value.(int); ok {
			if n < 1 || n > 10 {
				return "must be between 1 and 10"
			}
		}
	case "sleep":
		if n, ok := value.(f64); ok {
			if n < 0 || n > 24 {
				return "must be between 0.0 and 24.0"
			}
		}
	}
	return ""
}

Add_Options :: struct {
	mood:          int `args:"required" usage:"mood of the day, 1-10"`,
	sleep:         f64 `args:"required" usage:"the amount of slept hours 0.0-24.0"`,
	sleep_quality: int `args:"required" usage:"the previous night's sleep quality, 1-10"`,
	date:          time.Time `usage:"date to record this data to (defaults to today)"`,
	notes:         string `usage:"any extra notes for the day"`,
}

command_add :: proc(args: []string, store: ^core.Storage) {
	opt: Add_Options
	flags.register_flag_checker(validate_ranges)
	flags.parse_or_exit(&opt, args, .Unix)
	if time.time_to_unix(opt.date) == 0 {
		opt.date = time.now()
	}
	// FIXME: remove timezone offset
	if err := core.storage_add_to_daily_log(store^, opt.mood, opt.sleep, opt.sleep_quality, opt.date, opt.notes);
	   err != nil {
		fmt.eprintf("failed adding log: %v\n", err)
	}

	tz_date := core.new_time_with_timezone_offset(opt.date)
	buf: [time.MIN_YYYY_DATE_LEN]u8
	fmt.printf(
		"  hoarded: daily log for %s %s✓%s\n",
		time.to_string_yyyy_mm_dd(tz_date, buf[:]),
		core.GREEN,
		core.RESET,
	)
}

Symptom_Options :: struct {
	symptom:  string `args:"required" usage:"felt symptom"`,
	severity: int `args:"required" usage:"the severity of the symptom, 1-10"`,
	date:     time.Time `usage:"date and time to record this data to (defaults to now, RFC 3339)"`,
	notes:    string `usage:"any extra notes for the day"`,
}

command_symptom :: proc(args: []string, store: ^core.Storage) {
	opt: Symptom_Options
	flags.register_flag_checker(validate_ranges)
	flags.parse_or_exit(&opt, args, .Unix)
	if time.time_to_unix(opt.date) == 0 {
		opt.date = time.now()
	}
	if err := core.storage_add_to_symptoms(store^, opt.symptom, opt.severity, opt.date, opt.notes); err != nil {
		fmt.eprintf("failed adding symptom: %v\n", err)
	}

	tz_date := core.new_time_with_timezone_offset(opt.date)
	date_buf: [time.MIN_YYYY_DATE_LEN]u8
	time_buf: [time.MIN_HMS_LEN]u8
	fmt.printf(
		"  hoarded: symptom for %s %s@%s %s %s✓%s\n",
		time.to_string_yyyy_mm_dd(tz_date, date_buf[:]),
		core.DIM,
		time.to_string_hms(tz_date, time_buf[:]),
		core.RESET,
		core.GREEN,
		core.RESET,
	)
}

Event_Options :: struct {
	kind:   string `args:"required" usage:"what kind of event"`,
	amount: f64 `usage:"the amount of event (if applicable, say 'coffe')"`,
	unit:   string `usage:"the unit of the amount (if applicable, say a 'cup' of coffe)"`,
	date:   time.Time `usage:"date and time to record this data to (defaults to now, RFC 3339)"`,
}

// FIXME: Simplify, this is ridiculous
command_coffee :: proc(args: []string, store: ^core.Storage) {
	command_event({"event", "--kind", "coffee", "--amount", "1", "--unit", "cup"}, store)
}

// FIXME: Only known kinds
// FIXME: Limit valid units?
// FIXME: Validate that unit is set if amount is set
command_event :: proc(args: []string, store: ^core.Storage) {
	opt: Event_Options
	flags.parse_or_exit(&opt, args, .Unix)
	if time.time_to_unix(opt.date) == 0 {
		opt.date = time.now()
	}
	if err := core.storage_add_to_events(store^, opt.kind, opt.amount, opt.unit, opt.date); err != nil {
		fmt.eprintf("failed adding event: %v\n", err)
	}

	tz_date := core.new_time_with_timezone_offset(opt.date)
	date_buf: [time.MIN_YYYY_DATE_LEN]u8
	time_buf: [time.MIN_HMS_LEN]u8
	fmt.printf(
		"  hoarded: event for %s %s@%s %s %s✓%s\n",
		time.to_string_yyyy_mm_dd(tz_date, date_buf[:]),
		core.DIM,
		time.to_string_hms(tz_date, time_buf[:]),
		core.RESET,
		core.GREEN,
		core.RESET,
	)
}

command_today :: proc(args: []string, store: ^core.Storage) {
	buf: [time.MIN_YYYY_DATE_LEN]u8
	command_show({"show", time.to_string_yyyy_mm_dd(time.now(), buf[:])}, store)
}

SPARK_CHARS :: []string{"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}

spark_char :: proc(val, max: int) -> string {
	spark_chars := SPARK_CHARS
	idx := int((f64(val) / f64(max)) * f64(len(SPARK_CHARS) - 1))
	return spark_chars[clamp(idx, 0, len(SPARK_CHARS) - 1)]
}

spark_days :: proc(values: []int) -> string {
	b, err := strings.builder_make(context.temp_allocator)
	ensure(err == nil)
	defer strings.builder_destroy(&b)

	num_values := len(values)
	for v in values {
		if v < 0 {
			strings.write_string(&b, core.DIM)
			strings.write_string(&b, "·   ")
			strings.write_string(&b, core.RESET)
			continue
		}

		color := 31
		p := f64(v) / 10
		if p > 0.25 {
			color = 33
		}
		if p > 0.75 {
			color = 32
		}
		strings.write_string(&b, fmt.tprintf("\e[%dm", color))
		strings.write_string(&b, spark_char(v, 10))
		strings.write_string(&b, "   ")
		strings.write_string(&b, core.RESET)
	}
	strings.write_string(&b, core.DIM)
	for _ in num_values ..< 7 {
		strings.write_string(&b, "·   ")
	}
	strings.write_string(&b, core.RESET)

	return strings.to_string(b)
}

// FIXME: naive, does not take gaps in days into account
command_week :: proc(args: []string, store: ^core.Storage) {
	day := time.Hour * 24

	today := time.now()
	weekday := int(time.weekday(today))
	// NOTE: Corrects for Sunday being first day of the week in the library
	weekday = (weekday + int(time.Weekday.Saturday)) % (int(time.Weekday.Saturday) + 1)

	week_start := time.time_add(today, -(day * time.Duration(weekday)))
	week_end := time.time_add(week_start, day * 6)
	week_start_buf: [time.MIN_YYYY_DATE_LEN]u8
	week_end_buf: [time.MIN_YYYY_DATE_LEN]u8
	week_start_ymd := time.to_string_yyyy_mm_dd(week_start, week_start_buf[:])
	week_end_ymd := time.to_string_yyyy_mm_dd(week_end, week_end_buf[:])

	fmt.printf(
		"%s────────────────────────────────────────%s\n",
		core.DIM,
		core.RESET,
	)
	fmt.printf("%s◆ week of %s - %s%s\n", core.YELLOW, week_start_ymd, week_end_ymd, core.RESET)

	if data, get_err := core.storage_get_daily_log_span(store^, week_start, week_end);
	   len(data) == 0 || get_err != nil {
		fmt.printf("  %sno data for this week%s\n", core.RED, core.RESET)
	} else {
		fmt.printf("\n%sDAILY LOG%s\n", core.DIM, core.RESET)
		fmt.printf("\n\t\t%smon tue wed thu fri sat sun%s\n", core.DIM, core.RESET)

		current := week_start
		current_buf: [time.MIN_YYYY_DATE_LEN]u8
		current_str_ymd := time.to_string_yyyy_mm_dd(current, current_buf[:])
		mood_values: [dynamic; 7]int
		sleep_values: [dynamic; 7]int
		sleep_quality_values: [dynamic; 7]int
		for d in data {
			for d.date != current_str_ymd {
				append(&mood_values, -1)
				append(&sleep_values, -1)
				append(&sleep_quality_values, -1)
				current = time.time_add(current, day)
				current_str_ymd = time.to_string_yyyy_mm_dd(current, current_buf[:])
			}
			append(&mood_values, d.mood)
			append(&sleep_values, int((d.sleep / 8) * 10))
			append(&sleep_quality_values, d.sleep_quality)
			current = time.time_add(current, day)
			current_str_ymd = time.to_string_yyyy_mm_dd(current, current_buf[:])
		}

		fmt.printf("  %smood%s\t\t%s%s\n", core.DIM, core.RESET, spark_days(mood_values[:]), core.RESET)
		fmt.printf("  %ssleep%s\t\t%s%s\n", core.DIM, core.RESET, spark_days(sleep_values[:]), core.RESET)
		fmt.printf(
			"  %ssleep quality%s\t%s%s\n",
			core.DIM,
			core.RESET,
			spark_days(sleep_quality_values[:]),
			core.RESET,
		)
	}

	fmt.printf(
		"%s────────────────────────────────────────%s\n",
		core.DIM,
		core.RESET,
	)
}

build_bar :: proc(b: ^strings.Builder, val, max, width: int, invert := false) -> string {
	strings.builder_reset(b)

	filled := int(f64(val * width) / f64(max))
	p := f64(filled) / f64(width)
	for i in 0 ..< width {
		color := 31 if !invert else 32
		if p > 0.25 {
			color = 33
		}
		if p > 0.75 {
			color = 32 if !invert else 31
		}
		strings.write_string(b, fmt.tprintf("\e[%s%dm", "2;" if i >= filled else "", color))
		strings.write_rune(b, '█')
		strings.write_string(b, core.RESET)
	}

	return strings.to_string(b^)
}

command_show :: proc(args: []string, store: ^core.Storage) {
	if len(args) < 2 {
		// TODO: print usage
		return
	}

	date_parts := strings.split(args[1], "-", context.temp_allocator)
	yyyy, _ := strconv.parse_i64(date_parts[0])
	mm, _ := strconv.parse_i64(date_parts[1])
	dd, _ := strconv.parse_i64(date_parts[2])
	date, ok := time.components_to_time(yyyy, mm, dd, 0, 0, 0)
	ensure(ok)
	// TODO: print usage/invalid date

	b, err := strings.builder_make_len(10)
	ensure(err == nil)
	defer strings.builder_destroy(&b)

	fmt.printf(
		"%s────────────────────────────────────────%s\n",
		core.DIM,
		core.RESET,
	)
	day_name := strings.to_lower(fmt.tprintf("%s", time.weekday(date)), context.temp_allocator)
	fmt.printf("%s◆ %s, %s%s\n", core.YELLOW, day_name, args[1], core.RESET)

	if data, get_err := core.storage_get_daily_log(store^, date); get_err != nil {
		fmt.printf("  %sno daily data for this day%s\n", core.RED, core.RESET)
	} else {
		fmt.printf("\n%sDAILY LOG%s\n", core.DIM, core.RESET)
		fmt.printf(
			"  %smood%s\t\t%s %d/%s10%s\n",
			core.DIM,
			core.RESET,
			build_bar(&b, data.mood, 10, 10),
			data.mood,
			core.DIM,
			core.RESET,
		)
		fmt.printf(
			"  %ssleep%s\t\t%s %.1f%sh%s\n",
			core.DIM,
			core.RESET,
			build_bar(&b, int(data.sleep * 10), 80, 10),
			data.sleep,
			core.DIM,
			core.RESET,
		)
		fmt.printf(
			"  %ssleep quality%s\t%s %d/%s10%s\n",
			core.DIM,
			core.RESET,
			build_bar(&b, data.sleep_quality, 10, 10),
			data.sleep_quality,
			core.DIM,
			core.RESET,
		)
		fmt.printf("  %snotes%s\t\t%s\n", core.DIM, core.RESET, data.notes)
	}

	if data, get_err := core.storage_get_daily_symptoms(store^, date); get_err == nil {
		fmt.printf("\n%sSYMPTOMS%s\n", core.DIM, core.RESET)
		for d in data {
			fmt.printf(
				"  %s%s%s\t%s %d/%s10%s\n",
				core.DIM,
				d.symptom,
				core.RESET,
				build_bar(&b, d.severity, 10, 10, true),
				d.severity,
				core.DIM,
				core.RESET,
			)
		}
	}

	if data, get_err := core.storage_get_daily_events(store^, date); get_err == nil {
		fmt.printf("\n%sEVENTS%s\n", core.DIM, core.RESET)
		for d in data {
			tz_time := core.new_time_with_timezone_offset(d.timestamp)
			hh, mm, _ := time.clock_from_time(tz_time)
			icon: string
			switch d.kind {
			case "coffee":
				icon = "☕ "
			case "walk":
				icon = "🚶 "
			case "painkiller":
				icon = "💊 "
			}
			amount := fmt.tprintf("%s · %.1f %s%s", core.DIM, d.amount, d.unit, core.RESET) if d.amount > 1 else ""
			fmt.printf("  %s%02d:%02d%s %s%s%s\n", core.DIM, hh, mm, core.RESET, icon, d.kind, amount)
		}
	}

	fmt.printf(
		"%s────────────────────────────────────────%s\n",
		core.DIM,
		core.RESET,
	)
}

command_events :: proc(args: []string, store: ^core.Storage) {
	fmt.printf("events\n")
	fmt.printf("args: %v\n", args)
}

command_version :: proc(args: []string, store: ^core.Storage) {
	fmt.printf("version\n")
	fmt.printf("args: %v\n", args)
}