package files

import (
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.burning.moe/celediel/gt/internal/filter"

	"github.com/charmbracelet/log"
	"github.com/dustin/go-humanize"
)

type DiskFile struct {
	name, path string
	filesize   int64
	modified   time.Time
	isdir      bool
	mode       fs.FileMode
}

func (f DiskFile) Name() string      { return f.name }
func (f DiskFile) Path() string      { return f.path }
func (f DiskFile) Date() time.Time   { return f.modified }
func (f DiskFile) IsDir() bool       { return f.isdir }
func (f DiskFile) Mode() fs.FileMode { return f.mode }
func (f DiskFile) Filesize() int64 {
	if f.isdir {
		return 0
	}
	return f.filesize
}

func (f DiskFile) String() string {
	// this is unique enough because two files can't be named the same in the same directory
	// right???
	return f.name + f.path
}

func NewDisk(path string) (DiskFile, error) {
	info, err := os.Lstat(path)
	if err != nil {
		return DiskFile{}, err
	}

	abs, err := filepath.Abs(path)
	if err != nil {
		log.Errorf("couldn't get absolute path for %s", path)
		abs = path
	}

	name := filepath.Base(abs)
	basePath := filepath.Dir(abs)

	log.Debugf("%s (base:%s) (size:%s) (modified:%s) exists",
		name, basePath, humanize.Bytes(uint64(info.Size())), info.ModTime())

	return DiskFile{
		name:     name,
		path:     filepath.Join(basePath, name),
		filesize: info.Size(),
		modified: info.ModTime(),
		isdir:    info.IsDir(),
		mode:     info.Mode(),
	}, nil
}

func FindDisk(dir string, recursive bool, fltr *filter.Filter) (Files, error) {
	var files Files
	dir = filepath.Clean(dir)
	if dir == "." || dir == "" {
		if pwd, err := os.Getwd(); err != nil {
			dir = filepath.Clean(dir)
		} else {
			dir = pwd
		}
	}

	var recursively string
	if recursive {
		recursively = " recursively"
	}

	log.Debugf("gonna find files%s in %s matching %s", recursively, dir, fltr)

	if recursive {
		files = append(files, walkDir(dir, fltr)...)
	} else {
		files = append(files, readDir(dir, fltr)...)
	}

	return files, nil
}

// isInHiddenDir checks `path` and parent directories
// of `path` up to `base` for a hidden parent.
func isInHiddenDir(base, path string) bool {
	current := path
	for {
		current = filepath.Clean(current)
		if current == base {
			break
		}
		if strings.HasPrefix(filepath.Base(current), ".") {
			return true
		}
		current += string(os.PathSeparator) + ".."
	}
	return false
}

func walkDir(dir string, fltr *filter.Filter) Files {
	var files Files
	err := filepath.WalkDir(dir, func(path string, dirEntry fs.DirEntry, err error) error {
		if dir == path {
			return nil
		}

		if isInHiddenDir(dir, path) && fltr.IgnoreHidden() {
			return nil
		}

		actualPath, e := filepath.Abs(path)
		if e != nil {
			return err
		}

		name := dirEntry.Name()
		info, _ := dirEntry.Info()
		if fltr.Match(info) {
			files = append(files, DiskFile{
				path:     actualPath,
				name:     name,
				filesize: info.Size(),
				modified: info.ModTime(),
				isdir:    info.IsDir(),
				mode:     info.Mode(),
			})
		}
		return nil
	})
	if err != nil {
		log.Errorf("error walking directory %s: %s", dir, err)
		return Files{}
	}
	return files
}

func readDir(dir string, fltr *filter.Filter) Files {
	var files Files
	fs, err := os.ReadDir(dir)
	if err != nil {
		return Files{}
	}
	for _, file := range fs {
		name := file.Name()

		if name == dir {
			continue
		}

		info, err := file.Info()
		if err != nil {
			return Files{}
		}

		path := filepath.Dir(filepath.Join(dir, name))

		if fltr.Match(info) {
			files = append(files, DiskFile{
				name:     name,
				path:     filepath.Join(path, name),
				modified: info.ModTime(),
				filesize: info.Size(),
				isdir:    info.IsDir(),
				mode:     info.Mode(),
			})
		}
	}
	return files
}
