package main

import (
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.wow.st/gmp/logbook"
	"git.wow.st/gmp/logbook/parser"
	"git.wow.st/gmp/logbook/review"
)

func expandPath(path string) string {
	if strings.HasPrefix(path, "~/") {
		home, err := os.UserHomeDir()
		if err == nil {
			return filepath.Join(home, path[2:])
		}
	}
	return path
}

func usage() {
	fmt.Fprintf(os.Stderr, `Usage:
  lb [flags] <command> [args]

Commands:
  import [file]          Import workouts from DSL file (or stdin if no file)
  list                   List all workouts in the database
  list-measurements      List all measurements in the database
  performances [exercise] [variation] List past performances on specified exercise
  predict date variable  Predict the value of variable on date
  delete [all][workouts][measurements] delete specified items

Flags:
  -db <path>       Path to BoltDB database file (default: ~/.config/logbook/logbook.db)
`)
}

func getPerformances(wr *logbook.WorkoutRepository, mr *logbook.MeasurementRepository) ([]review.Performance, error) {
	pr := review.NewPerformanceRepository(wr, mr)
	filter := func(p review.Performance) bool { return true }
	switch flag.NArg() {
	case 1:
	case 2:
		filter = func(p review.Performance) bool {
			if p.Exercise == flag.Arg(1) {
				return true
			} else {
				return false
			}
		}
	case 3:
		filter = func(p review.Performance) bool {
			if p.Exercise == flag.Arg(1) && p.Type == flag.Arg(2) {
				return true
			} else {
				return false
			}
		}
	default:
		usage()
		os.Exit(1)
	}

	ps, err := pr.FindPerformances(filter)
	return ps, err
}

func main() {
	defaultDB := "~/.config/logbook/logbook.db"
	var dbPath string
	flag.StringVar(&dbPath, "db", defaultDB, "Path to BoltDB database file")
	flag.Usage = usage
	flag.Parse()

	if flag.NArg() == 0 {
		usage()
		os.Exit(1)
	}

	dbPath = expandPath(dbPath)

	wr, mr, err := logbook.NewRepositories(dbPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error opening repositories: %v\n", err)
		os.Exit(1)
	}
	defer wr.Close()
	defer mr.Close()

	cmd := flag.Arg(0)
	switch cmd {
	case "import":
		var input io.Reader
		if flag.NArg() > 1 {
			f, err := os.Open(flag.Arg(1))
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err)
				os.Exit(1)
			}
			defer f.Close()
			input = f
		} else {
			input = os.Stdin
		}

		dslBytes, err := io.ReadAll(input)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
			os.Exit(1)
		}
		dslText := string(dslBytes)

		workouts, measurements, err := parser.ParseDSL(dslText)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Parse error: %v\n", err)
			os.Exit(1)
		}

		if err := wr.AddWorkoutsFromIR(workouts); err != nil {
			fmt.Fprintf(os.Stderr, "Database write error: %v\n", err)
			os.Exit(1)
		}

		if err := mr.AddMeasurementsFromIR(measurements); err != nil {
			fmt.Fprintf(os.Stderr, "Database write error: %v\n", err)
			os.Exit(1)
		}

		fmt.Printf("Successfully imported %d workout(s) and %d measurement(s).\n", len(workouts), len(measurements))

	case "list":
		workouts, _, err := wr.FindWorkouts(func(w *parser.Workout) bool { return true })
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading workouts: %v\n", err)
			os.Exit(1)
		}
		if len(workouts) == 0 {
			fmt.Println("No workouts found.")
			return
		}

		for _, w := range workouts {
			dateStr := w.Date.Format("2 Jan 2006")
			fmt.Printf("Workout: %s (%s) %d SetGroups.", dateStr, w.Type, len(w.SetGroups))
			if w.Note != "" {
				fmt.Printf(" \"%s\"", w.Note)
			}
			fmt.Println()
			for _, sg := range w.SetGroups {
				fmt.Printf("Setgroup: %s", sg.Exercise)
				if sg.Type != "" {
					fmt.Printf(" (%s)", sg.Type)
				}
				fmt.Printf(" %d planned sets.", len(sg.PlannedSets))
				if sg.Note != "" {
					fmt.Printf(" \"%s\"", sg.Note)
				}
				fmt.Println()
				for _, s := range sg.PlannedSets {
					fmt.Printf("Planned Set: %.0fx%d", s.Weight, s.Reps)
					if s.Note != "" {
						fmt.Printf(" (%s)", s.Note)
					}
					fmt.Println()
				}
				if len(sg.ActualSets) > 0 {
					for _, s := range sg.ActualSets {
						fmt.Printf("Actual Set: %.0fx%d (%d RIR)", s.Weight, s.Reps, s.RIR)
						if s.Note != "" {
							fmt.Printf(" (%s)", s.Note)
						}
						fmt.Println()
					}
				}
			}
		}

	case "list-measurements":
		ms, err := mr.FindAll()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error finding measurements: %v\n", err)
			os.Exit(1)
		}
		for _,m := range(ms) {
			dateStr := m.Date.Format("2 Jan 2006")
			fmt.Printf("Measurement: %s %s = %0.1f\n", dateStr, m.Variable, m.Value)
		}
	case "performances":
		ps, err := getPerformances(wr, mr)
		if err != nil {
			fmt.Printf("Error finding performance: %v\n", err)
			os.Exit(1)
		}
		for _, p := range (ps) {
			fmt.Printf("Performance: %v\n", p)
		}
	case "estimate": // show rep chart for an exercise
		ps, err := getPerformances(wr, mr)
		if err != nil {
			fmt.Printf("Error finding performance: %v\n", err)
			os.Exit(1)
		}
		var weights, reps []float64
		var dates []time.Time
		isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)

		for _, p := range (ps) {
			for i, w := range (p.Weights) {
				if isbw {
					w = w + p.Bodyweight
				}
				if p.RIR[i] == 0 {
					weights = append(weights, w)
					reps = append(reps, float64(p.Reps[i]))
					dates = append(dates, p.Times[i].UTC())
					fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
				}
			}
		}
		est := review.NewEstimator(review.WithHalfLife(14.0))
		err = est.Fit(weights, reps, dates)
		if err != nil {
			fmt.Printf("Error estimating performance: %v\n", err)
			os.Exit(1)
		}
		for i := 1; i<=10; i++ {
			adj := 0.0
			if isbw {
				adj = ps[len(ps)-1].Bodyweight
			}
			fmt.Printf("%d: %0.0f\n", i, est.EstimateMaxWeight(float64(i)) - adj)
		}
	case "weekly-estimate":
		ps, err := getPerformances(wr, mr)
		if err != nil {
			fmt.Printf("Error finding performance: %v\n", err)
			os.Exit(1)
		}
		var weights, reps []float64
		var dates []time.Time
		isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)

		for _, p := range (ps) {
			for i, w := range (p.Weights) {
				if isbw {
					w = w + p.Bodyweight
				}
				if p.RIR[i] == 0 {
					weights = append(weights, w)
					reps = append(reps, float64(p.Reps[i]))
					dates = append(dates, p.Times[i].UTC())
					fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
				}
			}
		}
		var adj float64
		if isbw {
			adj = ps[len(ps)-1].Bodyweight
		}
		est := review.NewEstimator(review.WithHalfLife(14.0))
		err = est.Fit(weights, reps, dates)
		if err != nil {
			fmt.Printf("Error estimating performance: %v\n", err)
			os.Exit(1)
		}
		times, vals := est.TimelineEstimate1RM(7*24*time.Hour)
		for i, t := range times {
			fmt.Printf("%s: %0.0f\n", t.Format("2006-01-02"), vals[i] - adj)
		}
	case "predict":
		if flag.NArg() != 3 {
			usage()
			os.Exit(1)
		}
		dateStr := flag.Arg(1)
		date, err := time.Parse("2006-01-02", dateStr)
		if err != nil {
			fmt.Printf("Invalid date: %v", err)
			os.Exit(1)
		}
		variable := flag.Arg(2)
		fmt.Printf("Predicting %s for date %s\n", variable, date.Format("2006-01-02"))

		p, err := mr.Predict(variable, date, 0.75)
		if err != nil {
			fmt.Printf("Prediction error: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("%f\n",p)
	case "delete":
		if flag.NArg() != 2 {
			usage()
			os.Exit(1)
		}
		switch flag.Arg(1) {
		case "workouts":
			_, ids, err := wr.FindWorkouts(func(*parser.Workout) bool { return true })
			if err != nil {
				fmt.Printf("Error finding workouts: %v\n", err)
				os.Exit(1)
			}
			for _, id := range(ids) {
				err = wr.DeleteWorkout(id)
				if err != nil {
					fmt.Printf("Error deleting workout: %v\n", err)
					os.Exit(1)
				}
			}
			fmt.Printf("%d workouts deleted.\n", len(ids))
		case "measurements":
			ms, err := mr.FindAll()
			if err != nil {
				fmt.Printf("Error finding measurements: %v\n", err)
				os.Exit(1)
			}
			for _, m := range(ms) {
				err = mr.Delete(m.ID)
				if err != nil {
					fmt.Printf("Error deleting workout: %v\n", err)
					os.Exit(1)
				}
			}
			fmt.Printf("%d measurements deleted.\n", len(ms))
		}
	default:
		usage()
		os.Exit(1)
	}
}