// goredo -- djb's redo implementation on pure Go
// Copyright (C) 2020-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

// Out-of-date determination

package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"os"
	"path"
	"strings"

	"golang.org/x/sys/unix"
)

const (
	EnvOODTgtsFd     = "REDO_OOD_TGTS_FD"
	EnvOODTgtsLockFd = "REDO_OOD_TGTS_LOCK_FD"
)

var (
	OODTgts       map[string]struct{}
	FdOODTgts     *os.File
	FdOODTgtsLock *os.File

	OODCache        = make(map[string]bool)
	FileExistsCache = make(map[string]bool)
)

func FileExists(p string) bool {
	if exists, known := FileExistsCache[p]; known {
		return exists
	}
	_, err := os.Stat(p)
	if err == nil {
		FileExistsCache[p] = true
		return true
	}
	if errors.Is(err, fs.ErrNotExist) {
		FileExistsCache[p] = false
	}
	return false
}

type TgtError struct {
	Tgt *Tgt
	Err error
}

func (e TgtError) Unwrap() error { return e.Err }

func (e TgtError) Error() string {
	return fmt.Sprintf("%s: %s", e.Tgt, e.Err)
}

func isSrc(tgt *Tgt) bool {
	if !FileExists(tgt.a) {
		return false
	}
	tgtH, tgtT := pathSplit(tgt.a)
	if do, _, _ := findDo(nil, tgt.a, tgtH, tgtT); do != "" {
		return false
	}
	return true
}

func isOODByBuildUUID(tgt *Tgt) bool {
	build, err := depBuildRead(tgt.dep)
	return err != nil || build != BuildUUID
}

func isOOD(tgt *Tgt, level int, seen map[string]*Tgt) (bool, error) {
	indent := strings.Repeat(". ", level)
	tracef(CDebug, "ood: %s%s checking", indent, tgt)
	ood, cached := OODCache[tgt.rel]
	if cached {
		tracef(CDebug, "ood: %s%s -> cached: %v", indent, tgt, ood)
		return ood, nil
	}
	dep := DepCache[tgt.rel]
	var err error
	if dep == nil {
		dep, err = depRead(tgt)
		if err != nil {
			if errors.Is(err, fs.ErrNotExist) {
				if isSrc(tgt) {
					ood = false
					tracef(CDebug, "ood: %s%s -> is source", indent, tgt)
				} else if path.Ext(tgt.a) == ".do" {
					ood = false
					tracef(CDebug, "ood: %s%s -> is .do", indent, tgt)
				} else {
					ood = true
					tracef(CDebug, "ood: %s%s -> no dep: %s", indent, tgt, tgt.dep)
				}
				OODCache[tgt.rel] = ood
				return ood, nil
			}
			return true, TgtError{tgt, ErrLine(err)}
		}
		DepCache[tgt.rel] = dep
	}

	if dep.build == BuildUUID {
		tracef(CDebug, "ood: %s%s -> already built", indent, tgt)
		OODCache[tgt.rel] = false
		return false, nil
	}
	if !FileExists(tgt.a) {
		tracef(CDebug, "ood: %s%s -> non-existent", indent, tgt)
		OODCache[tgt.rel] = true
		return true, nil
	}

	for _, ifcreate := range dep.ifcreates {
		if FileExists(ifcreate.a) {
			tracef(CDebug, "ood: %s%s -> %s created", indent, tgt, ifcreate)
			ood = true
			goto Done
		}
	}

	for _, ifchange := range dep.ifchanges {
		tracef(CDebug, "ood: %s%s -> %s: checking", indent, tgt, ifchange.tgt)
		ood, cached = OODCache[ifchange.tgt.rel]
		if cached {
			tracef(CDebug, "ood: %s%s -> %s: cached: %v", indent, tgt, ifchange.tgt, ood)
			if ood {
				goto Done
			}
			continue
		}

		inode, err := inodeFromFileByPath(ifchange.tgt.a)
		if err != nil {
			if errors.Is(err, fs.ErrNotExist) {
				tracef(CDebug, "ood: %s%s -> %s: not exists", indent, tgt, ifchange.tgt)
				ood = true
				OODCache[ifchange.tgt.rel] = ood
				goto Done
			}
			return ood, TgtError{tgt, ErrLine(err)}
		}

		if !bytes.Equal(inode[:8], ifchange.Inode()[:8]) {
			tracef(CDebug, "ood: %s%s -> %s: size differs", indent, tgt, ifchange.tgt)
			ood = true
			OODCache[ifchange.tgt.rel] = ood
			goto Done
		}
		if InodeTrust != InodeTrustNone && inode.Equals(ifchange.Inode()) {
			tracef(CDebug, "ood: %s%s -> %s: same inode", indent, tgt, ifchange.tgt)
		} else {
			tracef(CDebug, "ood: %s%s -> %s: inode differs", indent, tgt, ifchange.tgt)
			var fd *os.File
			fd, err = os.Open(ifchange.tgt.a)
			if err != nil {
				return ood, TgtError{tgt, ErrLine(err)}
			}
			var hsh Hash
			hsh, err = fileHash(fd)
			fd.Close()
			if err != nil {
				return ood, TgtError{tgt, ErrLine(err)}
			}
			if ifchange.Hash() != hsh {
				tracef(CDebug, "ood: %s%s -> %s: hash differs", indent, tgt, ifchange.tgt)
				ood = true
				OODCache[ifchange.tgt.rel] = ood
				goto Done
			}
			tracef(CDebug, "ood: %s%s -> %s: same hash", indent, tgt, ifchange.tgt)
		}

		if ifchange.tgt.rel == tgt.rel {
			tracef(CDebug, "ood: %s%s -> %s: same target", indent, tgt, ifchange.tgt)
			continue
		}
		if isSrc(ifchange.tgt) {
			tracef(CDebug, "ood: %s%s -> %s: is source", indent, tgt, ifchange.tgt)
			OODCache[ifchange.tgt.rel] = false
			continue
		}

		if _, exists := seen[ifchange.tgt.rel]; exists {
			tracef(CDebug, "ood: %s%s -> %s: was always built", indent, tgt, ifchange.tgt)
			OODCache[ifchange.tgt.rel] = false
			continue
		}

		depOOD, err := isOODWithTrace(ifchange.tgt, level+1, seen)
		if err != nil {
			return ood, TgtError{tgt, err}
		}
		if depOOD {
			tracef(CDebug, "ood: %s%s -> %s: ood", indent, tgt, ifchange.tgt)
			ood = true
			goto Done
		}
		tracef(CDebug, "ood: %s%s -> %s: !ood", indent, tgt, ifchange.tgt)
	}

Done:
	tracef(CDebug, "ood: %s%s: %v", indent, tgt, ood)
	OODCache[tgt.rel] = ood
	return ood, nil
}

func isOODWithTrace(tgt *Tgt, level int, seen map[string]*Tgt) (bool, error) {
	_, ood := OODTgts[tgt.a]
	var err error
	if ood {
		if !isOODByBuildUUID(tgt) {
			tracef(CDebug, "ood: %s%s -> already built", strings.Repeat(". ", level), tgt)
			return false, nil
		}
		tracef(CDebug, "ood: %s%s true, external decision", strings.Repeat(". ", level), tgt)
		goto RecordOODTgt
	}
	ood, err = isOOD(tgt, level, seen)
	if !ood {
		return ood, err
	}
RecordOODTgt:
	flock := unix.Flock_t{
		Type:   unix.F_WRLCK,
		Whence: io.SeekStart,
	}
	if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLKW, &flock); err != nil {
		log.Fatal(err)
	}
	if _, err = FdOODTgts.Seek(0, io.SeekEnd); err != nil {
		log.Fatal(err)
	}
	if _, err = FdOODTgts.WriteString(tgt.a + "\x00"); err != nil {
		log.Fatal(err)
	}
	flock.Type = unix.F_UNLCK
	if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLK, &flock); err != nil {
		log.Fatal(err)
	}
	return true, nil
}

func oodTgtsClear() {
	var err error
	flock := unix.Flock_t{
		Type:   unix.F_WRLCK,
		Whence: io.SeekStart,
	}
	if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLKW, &flock); err != nil {
		log.Fatal(err)
	}
	if err = FdOODTgts.Truncate(0); err != nil {
		log.Fatal(err)
	}
	flock.Type = unix.F_UNLCK
	if err = unix.FcntlFlock(FdOODTgtsLock.Fd(), unix.F_SETLK, &flock); err != nil {
		log.Fatal(err)
	}
}
