// Copyright (C) 2022 Marius Schellenberger

package main

import (
	"cachefs/pkg/provider"
	"cachefs/pkg/provider/parse"
	"encoding/hex"
	"errors"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"time"
)

var (
	progress bool
)

const gib = 1024 * 1024 * 1024

const (
	srcKey = "SRC_KEY"
	dstKey = "DST_KEY"
)

func fatal(err error) {
	fmt.Println(err)
	os.Exit(1)
}

func getKey(key string) (b []byte, err error) {
	k := os.Getenv(key)
	if k == "" {
		return
	}
	b, err = hex.DecodeString(k)
	if err != nil {
		err = fmt.Errorf("error decoding %s: %w", key, err)
	}
	return
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
		fmt.Fprintf(flag.CommandLine.Output(), `
optional environment variables:

  `+srcKey+` (source encryption key)
  `+dstKey+` (destination encryption key)

list files and directories (uses `+srcKey+`):
  ls <base> [ <path> ]

  ls file:///mnt/nfs /foo

copy file from/to directory:
  cp <src base> <src> <dst base> <dst>

  cp file:///mnt/nfs foo.txt file:///mnt/cache bar.txt

  cp file:///mnt/cache fizz.txt sftp:///home/user fizz.txt

remove file (uses `+srcKey+`):
  rm <base> <file>

  rm file:///mnt/nfs /buzz.txt

options
  -p  show copy progress

`)
		os.Exit(1)
	}
	flag.BoolVar(&progress, "p", false, "show copy progress")
	flag.Parse()

	args := flag.Args()
	if len(args) < 2 {
		flag.Usage()
	}

	var err error
	switch args[0] {
	case "ls":
		err = ls(args[1:])
	case "cp":
		if len(args) < 5 {
			flag.Usage()
		}
		err = cp(args[1:])
	case "rm":
		if len(args) < 3 {
			flag.Usage()
		}
		err = rm(args[1:])
	}
	if err != nil {
		fatal(err)
	}
}

func ls(args []string) (err error) {
	k, err := getKey(srcKey)
	if err != nil {
		return
	}
	fs, err := parse.FS(args[0], k, true)
	if err != nil {
		return
	}
	defer fs.Close()
	path := ""
	if len(args) == 2 {
		path = args[1]
	}
	f, err := fs.Open(path)
	if err != nil {
		return
	}
	defer f.Close()
	fis, err := f.Readdir(0)
	if err != nil {
		return
	}
	for _, fi := range fis {
		p := filepath.Join(fs.Root(), path, fi.Name())
		if fi.IsDir() {
			fmt.Println(fi.Mode().String(), fi.Size(), p+"/")
			continue
		}
		fmt.Println(fi.Mode().String(), fi.Size(), p)
	}
	return
}

func cp(args []string) (err error) {
	src := args[0]
	sp := args[1]
	dst := args[2]
	dp := args[3]
	sk, err := getKey(srcKey)
	if err != nil {
		return
	}
	dk, err := getKey(dstKey)
	if err != nil {
		return
	}
	sfs, err := parse.FS(src, sk, true)
	if err != nil {
		return
	}
	defer sfs.Close()
	dfs, err := parse.FS(dst, dk, true)
	if err != nil {
		return
	}
	defer dfs.Close()

	sf, err := sfs.Open(sp)
	if err != nil {
		return
	}
	defer sf.Close()
	if i := strings.LastIndex(dp, "/"); i > 0 {
		dir := dp[:i]
		_, err = dfs.Stat(dir)
		if errors.Is(err, os.ErrNotExist) {
			err = dfs.MkdirAll(dir, 0o755)
			if err != nil {
				return
			}
		}
	}
	df, err := dfs.OpenFile(dp, os.O_RDWR|os.O_CREATE, 0o644)
	if err != nil {
		return
	}
	defer df.Close()
	if progress {
		fi, err := sf.Stat()
		if err != nil {
			return err
		}
		r := &progressReader{
			f:    sf,
			done: make(chan struct{}),
			size: fi.Size(),
		}
		go r.Progress()
		_, err = io.Copy(df, r)
		close(r.done)
		r.Print()
		fmt.Println()
	} else {
		_, err = io.Copy(df, sf)
	}
	return
}

type progressReader struct {
	f    provider.File
	done chan struct{}
	size int64
	n    int64
}

func (p *progressReader) Read(b []byte) (n int, err error) {
	n, err = p.f.Read(b)
	atomic.AddInt64(&p.n, int64(n))
	return
}

func (p *progressReader) ReadAt(b []byte, off int64) (n int, err error) {
	n, err = p.f.ReadAt(b, off)
	atomic.AddInt64(&p.n, int64(n))
	return
}

func (p *progressReader) Print() {
	n := atomic.LoadInt64(&p.n)
	percent := int(math.Round(float64(n) / float64(p.size) * 100))
	fmt.Printf("progress: %d / %d %d%%\r", n, p.size, percent)
}

func (p *progressReader) Progress() {
	for {
		select {
		case <-p.done:
			return
		case <-time.After(time.Millisecond * 500):
		}
		p.Print()
	}
}

func rm(args []string) (err error) {
	src := args[0]
	path := args[1]
	k, err := getKey(srcKey)
	if err != nil {
		return
	}
	fs, err := parse.FS(src, k, true)
	if err != nil {
		return
	}
	defer fs.Close()
	return fs.Remove(path)
}
