package pkg

import (
	"errors"
	"os"
	"unique"
)

// Clean destroys checksum backing entries without any identifier or substitute
// entry referring to it. If at least one keep [Artifact] is specified,
// identifier and substitute entries not kept alive by them are destroyed first.
func (c *Cache) Clean(dry, inputs bool, keep ...Artifact) (
	[]unique.Handle[ID],
	[]unique.Handle[Checksum],
	error,
) {
	c.identMu.Lock()
	defer c.identMu.Unlock()

	c.checksumMu.Lock()
	defer c.checksumMu.Unlock()

	dents, err := os.ReadDir(c.base.Append(dirChecksum).String())
	if err != nil {
		return nil, nil, err
	}
	checksums := make(map[unique.Handle[Checksum]]string, len(dents))
	var buf Checksum
	for _, dent := range dents {
		name := dent.Name()
		if err = Decode(&buf, name); err != nil {
			return nil, nil, err
		}
		checksums[unique.Make(buf)] = name
	}

	type identPair struct {
		id   unique.Handle[ID]
		name string
	}
	dents, err = os.ReadDir(c.base.Append(dirIdentifier).String())
	if err != nil {
		return nil, nil, err
	}

	keepIdents := make(map[unique.Handle[ID]]struct{})
	if inputs {
		for _, id := range Inputs((*Collect)(&keep)) {
			keepIdents[id] = struct{}{}
		}
	} else {
		for _, a := range keep {
			keepIdents[c.Ident(a)] = struct{}{}
		}
	}

	idents := make([]identPair, 0, len(dents))
	for _, dent := range dents {
		name := dent.Name()
		if err = Decode(&buf, name); err != nil {
			return nil, nil, err
		}
		id := unique.Make(ID(buf))

		if _, ok := keepIdents[id]; len(keep) == 0 || ok {
			if err = readlinkChecksum(c.base.Append(
				dirIdentifier,
				name,
			), &buf); err != nil {
				return nil, nil, err
			}
			delete(checksums, unique.Make(buf))
			continue
		}

		c.msg.Verbosef(
			"arranging for destruction of %s%s%s...",
			c.sgrIdent, name, c.sgrRes,
		)
		idents = append(idents, identPair{id, name})
	}

	destroyedIdents := make([]unique.Handle[ID], 0, len(idents))
	for _, pair := range idents {
		if !dry {
			if err = os.Remove(c.base.Append(
				dirStatus,
				pair.name,
			).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
				return destroyedIdents, nil, err
			}

			if err = os.Remove(c.base.Append(
				dirIdentifier,
				pair.name,
			).String()); err != nil {
				return destroyedIdents, nil, err
			}
		}
		destroyedIdents = append(destroyedIdents, pair.id)
	}

	destroyedChecksums := make([]unique.Handle[Checksum], 0, len(checksums))
	for checksum, name := range checksums {
		if err = c.parent.Err(); err != nil {
			return destroyedIdents, destroyedChecksums, err
		}
		c.msg.Verbosef(
			"destroying checksum %s%s%s...",
			c.sgrIdent, name, c.sgrRes,
		)
		if !dry {
			if err = errors.Join(removeAll(c.base.Append(
				dirChecksum,
				name,
			))); err != nil {
				return destroyedIdents, destroyedChecksums, err
			}
		}
		destroyedChecksums = append(destroyedChecksums, checksum)
	}

	dents, err = os.ReadDir(c.base.Append(dirSubstitute).String())
	if err != nil {
		return destroyedIdents, destroyedChecksums, err
	}
	for _, dent := range dents {
		name := dent.Name()
		if err = readlinkChecksum(c.base.Append(
			dirSubstitute,
			name,
		), &buf); err != nil {
			return destroyedIdents, destroyedChecksums, err
		}
		if _, ok := checksums[unique.Make(buf)]; !ok {
			continue
		}

		c.msg.Verbosef(
			"destroying substitute %s%s%s...",
			c.sgrIdent, name, c.sgrRes,
		)
		if !dry {
			if err = os.Remove(c.base.Append(
				dirStatus,
				name,
			).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
				return destroyedIdents, nil, err
			}

			if err = os.Remove(c.base.Append(
				dirSubstitute,
				name,
			).String()); err != nil {
				return destroyedIdents, destroyedChecksums, err
			}
		}
	}

	return destroyedIdents, destroyedChecksums, nil
}
