//go:build darwin || freebsd || linux
// +build darwin freebsd linux

package fuse

import (
	"bytes"
	"context"
	"crypto/sha256"
	"fmt"
	"path"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/restic/restic/internal/debug"
	"github.com/restic/restic/internal/restic"
)

type MetaDirData struct {
	// set if this is a symlink or a snapshot mount point
	linkTarget string
	snapshot   *restic.Snapshot
	// names is set if this is a pseudo directory
	names map[string]*MetaDirData
}

// SnapshotsDirStructure contains the directory structure for snapshots.
// It uses a paths and time template to generate a map of pathnames
// pointing to the actual snapshots. For templates that end with a time,
// also "latest" links are generated.
type SnapshotsDirStructure struct {
	root          *Root
	pathTemplates []string
	timeTemplate  string

	mutex sync.Mutex
	// "" is the root path, subdirectory paths are assembled as parent+"/"+childFn
	// thus all subdirectories are prefixed with a slash as the root is ""
	// that way we don't need path processing special cases when using the entries tree
	entries map[string]*MetaDirData

	hash      [sha256.Size]byte // Hash at last check.
	lastCheck time.Time
}

// NewSnapshotsDirStructure returns a new directory structure for snapshots.
func NewSnapshotsDirStructure(root *Root, pathTemplates []string, timeTemplate string) *SnapshotsDirStructure {
	return &SnapshotsDirStructure{
		root:          root,
		pathTemplates: pathTemplates,
		timeTemplate:  timeTemplate,
	}
}

// pathsFromSn generates the paths from pathTemplate and timeTemplate
// where the variables are replaced by the snapshot data.
// The time is given as suffix if the pathTemplate ends with "%T".
func pathsFromSn(pathTemplate string, timeTemplate string, sn *restic.Snapshot) (paths []string, timeSuffix string) {
	timeformat := sn.Time.Format(timeTemplate)

	inVerb := false
	writeTime := false
	out := make([]strings.Builder, 1)
	for _, c := range pathTemplate {
		if writeTime {
			for i := range out {
				out[i].WriteString(timeformat)
			}
			writeTime = false
		}

		if !inVerb {
			if c == '%' {
				inVerb = true
			} else {
				for i := range out {
					out[i].WriteRune(c)
				}
			}
			continue
		}

		var repl string
		inVerb = false
		switch c {
		case 'T':
			// lazy write; time might be returned as suffix
			writeTime = true
			continue

		case 't':
			if len(sn.Tags) == 0 {
				return nil, ""
			}
			if len(sn.Tags) != 1 {
				// needs special treatment: Rebuild the string builders
				newout := make([]strings.Builder, len(out)*len(sn.Tags))
				for i, tag := range sn.Tags {
					tag = filenameFromTag(tag)
					for j := range out {
						newout[i*len(out)+j].WriteString(out[j].String() + tag)
					}
				}
				out = newout
				continue
			}
			repl = sn.Tags[0]

		case 'i':
			repl = sn.ID().Str()

		case 'I':
			repl = sn.ID().String()

		case 'u':
			repl = sn.Username

		case 'h':
			repl = sn.Hostname

		default:
			repl = string(c)
		}

		// write replacement string to all string builders
		for i := range out {
			out[i].WriteString(repl)
		}
	}

	for i := range out {
		paths = append(paths, out[i].String())
	}

	if writeTime {
		timeSuffix = timeformat
	}

	return paths, timeSuffix
}

// Some tags are problematic when used as filenames:
//
//	""
//	".", ".."
//	anything containing '/'
//
// Replace all special character by underscores "_", an empty tag is also represented as a underscore.
func filenameFromTag(tag string) string {
	switch tag {
	case "", ".":
		return "_"
	case "..":
		return "__"
	}

	return strings.ReplaceAll(tag, "/", "_")
}

// determine static path prefix
func staticPrefix(pathTemplate string) (prefix string) {
	inVerb := false
	patternStart := -1
outer:
	for i, c := range pathTemplate {
		if !inVerb {
			if c == '%' {
				inVerb = true
			}
			continue
		}
		inVerb = false
		switch c {
		case 'i', 'I', 'u', 'h', 't', 'T':
			patternStart = i
			break outer
		}
	}
	if patternStart < 0 {
		// ignore patterns without template variable
		return ""
	}

	p := pathTemplate[:patternStart]
	idx := strings.LastIndex(p, "/")
	if idx < 0 {
		return ""
	}
	return p[:idx]
}

// uniqueName returns a unique name to be used for prefix+name.
// It appends -number to make the name unique.
func uniqueName(entries map[string]*MetaDirData, prefix, name string) string {
	newname := name
	for i := 1; ; i++ {
		if _, ok := entries[prefix+newname]; !ok {
			break
		}
		newname = fmt.Sprintf("%s-%d", name, i)
	}
	return newname
}

// makeDirs inserts all paths generated from pathTemplates and
// TimeTemplate for all given snapshots into d.names.
// Also adds d.latest links if "%T" is at end of a path template
func (d *SnapshotsDirStructure) makeDirs(snapshots restic.Snapshots) {
	entries := make(map[string]*MetaDirData)

	type mountData struct {
		sn         *restic.Snapshot
		linkTarget string // if linkTarget!= "", this is a symlink
		childFn    string
		child      *MetaDirData
	}

	// recursively build tree structure
	var mount func(path string, data mountData)
	mount = func(path string, data mountData) {
		e := entries[path]
		if e == nil {
			e = &MetaDirData{}
		}
		if data.sn != nil {
			e.snapshot = data.sn
			e.linkTarget = data.linkTarget
		} else {
			// intermediate directory, register as a child directory
			if e.names == nil {
				e.names = make(map[string]*MetaDirData)
			}
			if data.child != nil {
				e.names[data.childFn] = data.child
			}
		}
		entries[path] = e

		slashIdx := strings.LastIndex(path, "/")
		if slashIdx >= 0 {
			// add to parent dir, but without snapshot
			mount(path[:slashIdx], mountData{childFn: path[slashIdx+1:], child: e})
		}
	}

	// root directory
	mount("", mountData{})

	// insert pure directories; needed to get empty structure even if there
	// are no snapshots in these dirs
	for _, p := range d.pathTemplates {
		p = staticPrefix(p)
		if p != "" {
			mount(path.Clean("/"+p), mountData{})
		}
	}

	latestTime := make(map[string]time.Time)
	for _, sn := range snapshots {
		for _, templ := range d.pathTemplates {
			paths, timeSuffix := pathsFromSn(templ, d.timeTemplate, sn)
			for _, p := range paths {
				if p != "" {
					p = "/" + p
				}
				suffix := uniqueName(entries, p, timeSuffix)
				mount(path.Clean(p+suffix), mountData{sn: sn})
				if timeSuffix != "" {
					lt, ok := latestTime[p]
					if !ok || !sn.Time.Before(lt) {
						debug.Log("link (update) %v -> %v\n", p, suffix)
						// inject symlink
						mount(path.Clean(p+"/latest"), mountData{sn: sn, linkTarget: suffix})
						latestTime[p] = sn.Time
					}
				}
			}
		}
	}

	d.entries = entries
}

const minSnapshotsReloadTime = 60 * time.Second

// update snapshots if repository has changed
func (d *SnapshotsDirStructure) updateSnapshots(ctx context.Context) error {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	if time.Since(d.lastCheck) < minSnapshotsReloadTime {
		return nil
	}

	var snapshots restic.Snapshots
	err := d.root.cfg.Filter.FindAll(ctx, d.root.repo, d.root.repo, nil, func(_ string, sn *restic.Snapshot, _ error) error {
		if sn != nil {
			snapshots = append(snapshots, sn)
		}
		return nil
	})
	if err != nil {
		return err
	}

	// Sort snapshots ascending by time, using the id to break ties.
	// This needs to be done before hashing.
	sort.Slice(snapshots, func(i, j int) bool {
		si, sj := snapshots[i], snapshots[j]
		if si.Time.Equal(sj.Time) {
			return bytes.Compare(si.ID()[:], sj.ID()[:]) < 0
		}
		return si.Time.Before(sj.Time)
	})

	// We update the snapshots when the hash of their id's changes.
	h := sha256.New()
	for _, sn := range snapshots {
		h.Write(sn.ID()[:])
	}
	var hash [sha256.Size]byte
	h.Sum(hash[:0])

	if d.hash == hash {
		d.lastCheck = time.Now()
		return nil
	}

	err = d.root.repo.LoadIndex(ctx, nil)
	if err != nil {
		return err
	}

	d.lastCheck = time.Now()
	d.hash = hash
	d.makeDirs(snapshots)
	return nil
}

func (d *SnapshotsDirStructure) UpdatePrefix(ctx context.Context, prefix string) (*MetaDirData, error) {
	err := d.updateSnapshots(ctx)
	if err != nil {
		return nil, err
	}

	d.mutex.Lock()
	defer d.mutex.Unlock()
	return d.entries[prefix], nil
}
