package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"math/big"
	"os"
	"os/exec"
	"os/signal"
	"path"
	"slices"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"
)

func main() {
	ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
	defer can()

	if err := Run(ctx, os.Args[1:]); err != nil {
		panic(err)
	}
}

func Run(ctx context.Context, args []string) error {
	cams, err := lsd(args[0])
	if err != nil {
		return err
	}

	for _, cam := range cams {
		files, err := lsf(cam)
		if err != nil {
			return err
		} else if len(files) < 1 {
			continue
		}

		lastChunk := strings.Split(path.Base(files[len(files)-1]), ".")[0]
		files = slices.DeleteFunc(files, func(f string) bool {
			return strings.Contains(f, lastChunk)
		})
		if len(files) == 0 {
			continue
		}

		lastMovementAt := time.Unix(0, 0)
		prevF := files[0]
		for _, f := range files[1:] {
			prev, err := os.Stat(prevF)
			if err != nil {
				return err
			}

			if ok, err := hasMovement(ctx, prevF, f); err != nil {
				return err
			} else if ok {
				lastMovementAt = prev.ModTime()
			}

			target := "trash"
			if prev.ModTime().Before(lastMovementAt.Add(3 * time.Minute)) {
				target = "movement"
			}
			if target == "trash" {
				log.Println("deleting", prevF)
				if err := os.Remove(prevF); err != nil {
					return err
				}
			} else if err := func() error {
				gName := strings.ReplaceAll(prevF, "record", target)
				if gName == prevF {
					return fmt.Errorf("would overwrite original %s", prevF)
				}
				os.MkdirAll(path.Dir(gName), os.ModePerm)

				log.Println("moving", prevF, "to", gName)

				f, err := os.Open(prevF)
				if err != nil {
					return err
				}
				defer f.Close()

				g, err := os.Create(gName)
				if err != nil {
					return err
				}
				defer g.Close()

				if _, err := io.Copy(g, f); err != nil {
					return err
				}

				return os.Remove(prevF)
			}(); err != nil {
				return err
			}

			prevF = f
		}
	}
	return nil
}

func hasMovement(ctx context.Context, a, b string) (bool, error) {
	sizeOfCmd := exec.CommandContext(ctx, "identify", a)
	sizeOfOutput, err := sizeOfCmd.CombinedOutput()
	if err != nil {
		return false, fmt.Errorf("failed to identify %s: (%w) %s", a, err, sizeOfOutput)
	}
	hw := strings.Fields(string(sizeOfOutput))[2]
	h, err := strconv.ParseInt(strings.Split(hw, "x")[0], 10, 16)
	if err != nil {
		return false, err
	}
	w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16)
	if err != nil {
		return false, err
	}
	total := h * w

	compareCmd := exec.CommandContext(ctx, "compare", "-metric", "AE", "-fuzz", "15%", a, b, "/dev/null")
	compareOutput, _ := compareCmd.CombinedOutput()

	f, _, err := big.ParseFloat(string(compareOutput), 10, 0, big.ToNearestEven)
	if err != nil {
		return false, err
	}
	i := new(big.Int)
	f.Int(i)
	delta := i.Int64()

	percentPixelsChanged := int(100.0 * float64(delta) / float64(total))
	return percentPixelsChanged > 10, nil
}

func lsd(d string) ([]string, error) {
	return ls(d, true)
}

func lsf(d string) ([]string, error) {
	return ls(d, false)
}

func ls(d string, dirs bool) ([]string, error) {
	entries, err := os.ReadDir(d)

	results := make([]string, 0, len(entries))
	for i := range entries {
		if dirs == entries[i].IsDir() {
			results = append(results, path.Join(d, entries[i].Name()))
		}
	}
	sort.Strings(results)

	return results, err
}
