package mfer

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"path"
	"path/filepath"
	"strings"

	"github.com/spf13/afero"
	"sneak.berlin/go/mfer/internal/log"
)

type manifestFile struct {
	path string
	info fs.FileInfo
}

func (m *manifestFile) String() string {
	return fmt.Sprintf("<File \"%s\">", m.path)
}

type manifest struct {
	sourceFS      []afero.Fs
	files         []*manifestFile
	scanOptions   *ManifestScanOptions
	totalFileSize int64
	pbInner       *MFFile
	pbOuter       *MFFileOuter
	output        *bytes.Buffer
	ctx           context.Context
	errors        []*error
}

func (m *manifest) String() string {
	return fmt.Sprintf("<Manifest count=%d totalSize=%d>", len(m.files), m.totalFileSize)
}

// ManifestScanOptions configures behavior when scanning directories for manifest generation.
type ManifestScanOptions struct {
	IncludeDotfiles bool // Include files and directories starting with a dot (default: exclude)
	FollowSymLinks  bool // Resolve symlinks instead of skipping them
}

func (m *manifest) HasError() bool {
	return len(m.errors) > 0
}

func (m *manifest) AddError(e error) *manifest {
	m.errors = append(m.errors, &e)
	return m
}

func (m *manifest) WithContext(c context.Context) *manifest {
	m.ctx = c
	return m
}

func (m *manifest) addInputPath(inputPath string) error {
	abs, err := filepath.Abs(inputPath)
	if err != nil {
		return err
	}
	// FIXME check to make sure inputPath/abs exists maybe
	afs := afero.NewReadOnlyFs(afero.NewBasePathFs(afero.NewOsFs(), abs))
	return m.addInputFS(afs)
}

func (m *manifest) addInputFS(f afero.Fs) error {
	if m.sourceFS == nil {
		m.sourceFS = make([]afero.Fs, 0)
	}
	m.sourceFS = append(m.sourceFS, f)
	// FIXME do some sort of check on f here?
	return nil
}

// New creates an empty manifest.
func New() *manifest {
	m := &manifest{}
	return m
}

// NewFromPaths creates a manifest configured to scan the given filesystem paths.
func NewFromPaths(options *ManifestScanOptions, inputPaths ...string) (*manifest, error) {
	log.Dump(inputPaths)
	m := New()
	m.scanOptions = options
	for _, p := range inputPaths {
		err := m.addInputPath(p)
		if err != nil {
			return nil, err
		}
	}
	return m, nil
}

// NewFromFS creates a manifest configured to scan the given afero filesystem.
func NewFromFS(options *ManifestScanOptions, fs afero.Fs) (*manifest, error) {
	m := New()
	m.scanOptions = options
	err := m.addInputFS(fs)
	if err != nil {
		return nil, err
	}
	return m, nil
}

func (m *manifest) GetFileCount() int64 {
	if m.pbInner != nil {
		return int64(len(m.pbInner.Files))
	}
	return int64(len(m.files))
}

func (m *manifest) GetTotalFileSize() int64 {
	if m.pbInner != nil {
		var total int64
		for _, f := range m.pbInner.Files {
			total += f.Size
		}
		return total
	}
	return m.totalFileSize
}

// Files returns all file entries from a loaded manifest.
func (m *manifest) Files() []*MFFilePath {
	if m.pbInner == nil {
		return nil
	}
	return m.pbInner.Files
}

func pathIsHidden(p string) bool {
	tp := path.Clean(p)
	if strings.HasPrefix(tp, ".") {
		return true
	}
	for {
		d, f := path.Split(tp)
		if strings.HasPrefix(f, ".") {
			return true
		}
		if d == "" {
			return false
		}
		tp = d[0 : len(d)-1] // trim trailing slash from dir
	}
}

func (m *manifest) addFile(p string, fi fs.FileInfo, sfsIndex int) error {
	if !m.scanOptions.IncludeDotfiles && pathIsHidden(p) {
		return nil
	}
	if fi != nil && fi.IsDir() {
		// manifests contain only files, directories are implied.
		return nil
	}
	// FIXME test if 'fi' is already result of stat
	fileinfo, staterr := m.sourceFS[sfsIndex].Stat(p)
	if staterr != nil {
		return staterr
	}
	cleanPath := p
	if cleanPath[0:1] == "/" {
		cleanPath = cleanPath[1:]
	}
	nf := &manifestFile{
		path: cleanPath,
		info: fileinfo,
	}
	m.files = append(m.files, nf)
	m.totalFileSize = m.totalFileSize + fi.Size()
	return nil
}

func (m *manifest) Scan() error {
	// FIXME scan and whatever function does the hashing should take ctx
	for idx, sfs := range m.sourceFS {
		if sfs == nil {
			return errors.New("invalid source fs")
		}
		e := afero.Walk(sfs, "/", func(p string, info fs.FileInfo, err error) error {
			return m.addFile(p, info, idx)
		})
		if e != nil {
			return e
		}
	}
	return nil
}
