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 fmt.Errorf("failed to lsd %s: %w", args[0], err)
	}

	movementInterval, err := time.ParseDuration(args[1])
	if err != nil {
		return err
	}

	for _, cam := range cams {
		files, err := lsf(cam)
		if err != nil {
			return fmt.Errorf("failed to lsf %s: %w", cam, err)
		} else if len(files) < 1 {
			continue
		}

		series := []string{}
		for _, f := range files {
			series = append(series, strings.Split(path.Base(f), ".")[0])
		}
		sort.Strings(series)
		series = slices.Compact(series)
		series = slices.DeleteFunc(series, func(s string) bool {
			return path.Base(s) > time.Now().Add(-10*time.Minute).Format("2006-01-02T15-04-05.")
		})

		for _, series := range series {
			if err := func() error {
				seriesFiles := []string{}
				for _, file := range files {
					if strings.HasPrefix(path.Base(file), series) {
						seriesFiles = append(seriesFiles, file)
					}
				}
				if seriesHasMovement, err := seriesHasMovement(ctx, seriesFiles, movementInterval); err != nil {
					return err
				} else if seriesHasMovement {
					firstFileInfo, err := os.Stat(seriesFiles[0])
					if err != nil {
						return err
					}
					lastFileInfo, err := os.Stat(seriesFiles[len(seriesFiles)-1])
					if err != nil {
						return err
					}
					durationSeconds := lastFileInfo.ModTime().Sub(firstFileInfo.ModTime()).Seconds()
					frames := len(seriesFiles)
					fps := int(float64(frames) / durationSeconds)

					log.Println("found movement in", series, "over", durationSeconds, "seconds at", fps, "fps")

					outd := strings.ReplaceAll(cam, "record", "movement")
					os.MkdirAll(outd, os.ModePerm)
					cmd := exec.CommandContext(ctx,
						"ffmpeg",
						"-y",
						"-framerate", strconv.Itoa(fps),
						"-pattern_type", "glob",
						"-i", path.Join(cam, series)+".*.jpg",
						"-r", strconv.Itoa(fps),
						path.Join(outd, series+".mp4"),
					)
					if out, err := cmd.CombinedOutput(); err != nil {
						return fmt.Errorf("failed to persist series %s: (%w) %s", series, err, out)
					}
				} else {
					log.Println("no movement in", series)
				}
				for _, seriesFile := range seriesFiles {
					if err := os.Remove(seriesFile); err != nil {
						return fmt.Errorf("failed to rm series %s[%s]: %w", series, seriesFile, err)
					}
				}
				return nil
			}(); err != nil {
				return err
			}
		}
	}

	for _, cam := range cams {
		camMovementD := strings.ReplaceAll(cam, "record", "movement")

		movementFiles, err := lsf(camMovementD)
		if err != nil {
			return err
		}
		sort.Strings(movementFiles)

		var sizeOfCamDMiB int64
		for _, movementFile := range movementFiles {
			stat, err := os.Stat(movementFile)
			if err != nil {
				return err
			}
			fileSizeMiB := stat.Size() / 1024 / 1024
			sizeOfCamDMiB += fileSizeMiB
		}

		GiB100 := int64(100 * 1024)
		for sizeOfCamDMiB > GiB100 && len(movementFiles) > 0 {
			oldestFile := movementFiles[0]

			oldestFileStat, err := os.Stat(oldestFile)
			if err != nil {
				return err
			}
			oldestFileSizeMiB := oldestFileStat.Size() / 1024 / 1024

			log.Println("deleting old file", oldestFile)
			if err := os.Remove(oldestFile); err != nil {
				return err
			}

			sizeOfCamDMiB -= oldestFileSizeMiB
			movementFiles = movementFiles[1:]
		}
	}

	return nil
}

func seriesHasMovement(ctx context.Context, files []string, movementInterval time.Duration) (bool, error) {
	if len(files) < 1 {
		return false, nil
	}

	f := files[0]
	fStat, err := os.Stat(f)
	if err != nil {
		return false, err
	}
	fTime := fStat.ModTime()

	for i := 1; i < len(files); i++ {
		g := files[i]
		gStat, err := os.Stat(g)
		if err != nil {
			return false, err
		}
		gTime := gStat.ModTime()

		if gTime.Sub(fTime) < movementInterval {
			continue
		}

		if hasMovement, err := hasMovement(ctx, f, g); err != nil {
			return false, fmt.Errorf("failed to check for movement between %s and %s: %w", f, g, err)
		} else if hasMovement {
			return true, nil
		}

		f = g
		fTime = gTime
	}
	return false, 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, fmt.Errorf("failed parsing %s for HxW: %w", hw, err)
	}
	w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16)
	if err != nil {
		return false, fmt.Errorf("failed parsing %s for HxW: %w", hw, 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, fmt.Errorf("failed to parse %s for a number of changed pixels: %w", compareOutput, err)
	}
	i := new(big.Int)
	f.Int(i)
	delta := i.Int64()

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

func mv(wPath, rPath string) error {
	r, err := os.Open(rPath)
	if err != nil {
		return err
	}
	defer r.Close()

	os.MkdirAll(path.Dir(wPath), os.ModePerm)
	w, err := os.Create(wPath)
	if err != nil {
		return err
	}
	defer w.Close()

	if _, err := io.Copy(w, r); err != nil {
		w.Close()
		os.Remove(wPath)
		return err
	}

	return os.Remove(rPath)
}

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
}
