package buildpack

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/BurntSushi/toml"
	"github.com/pkg/errors"

	"github.com/buildpacks/lifecycle/launch"
	"github.com/buildpacks/lifecycle/log"
)

type LayersDir struct {
	Path      string
	layers    []Layer
	name      string
	Buildpack GroupElement
	Store     *StoreTOML
}

func ReadLayersDir(layersDir string, bp GroupElement, logger log.Logger) (LayersDir, error) {
	path := filepath.Join(layersDir, launch.EscapeID(bp.ID))
	logger.Debugf("Reading buildpack directory: %s", path)
	bpDir := LayersDir{
		name:      bp.ID,
		Path:      path,
		layers:    []Layer{},
		Buildpack: bp,
	}

	fis, err := os.ReadDir(path)
	if err != nil && !os.IsNotExist(err) {
		return LayersDir{}, err
	}

	names := map[string]struct{}{}
	var tomls []string
	for _, fi := range fis {
		logger.Debugf("Reading buildpack directory item: %s", fi.Name())
		if fi.IsDir() {
			bpDir.layers = append(bpDir.layers, *bpDir.NewLayer(fi.Name(), bp.API, logger))
			names[fi.Name()] = struct{}{}
			continue
		}
		if strings.HasSuffix(fi.Name(), ".toml") {
			tomls = append(tomls, filepath.Join(path, fi.Name()))
		}
	}

	for _, tf := range tomls {
		name := strings.TrimSuffix(filepath.Base(tf), ".toml")
		if name == "store" {
			var bpStore StoreTOML
			_, err := toml.DecodeFile(tf, &bpStore)
			if err != nil {
				return LayersDir{}, errors.Wrapf(err, "failed decoding store.toml for buildpack %q", bp.ID)
			}
			bpDir.Store = &bpStore
			continue
		}
		if name == "launch" {
			// don't treat launch.toml as a layer
			continue
		}
		if name == "build" {
			continue
		}
		if _, ok := names[name]; !ok {
			bpDir.layers = append(bpDir.layers, *bpDir.NewLayer(name, bp.API, logger))
		}
	}
	sort.Slice(bpDir.layers, func(i, j int) bool {
		return bpDir.layers[i].identifier < bpDir.layers[j].identifier
	})
	return bpDir, nil
}

func (d *LayersDir) FindLayers(f func(layer Layer) bool) []Layer {
	var selectedLayers []Layer
	for _, l := range d.layers {
		if f(l) {
			selectedLayers = append(selectedLayers, l)
		}
	}
	return selectedLayers
}

func MadeLaunch(l Layer) bool {
	md, err := l.Read()
	return err == nil && md.Launch
}

func MadeCached(l Layer) bool {
	md, err := l.Read()
	return err == nil && md.Cache
}

func Malformed(l Layer) bool {
	_, err := l.Read()
	return err != nil
}

func (d *LayersDir) NewLayer(name, buildpackAPI string, logger log.Logger) *Layer {
	return &Layer{
		layerDir: layerDir{
			path:       filepath.Join(d.Path, name),
			identifier: fmt.Sprintf("%s:%s", d.Buildpack.ID, name),
		},
		api:    buildpackAPI,
		logger: logger,
	}
}

type Layer struct { // FIXME: need to refactor so api and logger won't be part of this struct
	layerDir
	api    string
	logger log.Logger
}

type layerDir struct {
	// identifier takes the form "buildpack-id:layer-name" and is used for
	// sorting layers,
	// logging information about a layer, and
	// creating the temporary tar file that is the basis of the layer.
	identifier string
	// path takes the form <layers>/<buildpack-id>/<layer-name>
	path string
}

func (l *Layer) Name() string {
	return filepath.Base(l.path)
}

func (l *Layer) HasLocalContents() bool {
	_, err := os.ReadDir(l.path)

	return !os.IsNotExist(err)
}

func (l *Layer) Identifier() string {
	return l.identifier
}

func (l *Layer) Path() string {
	return l.path
}

func (l *Layer) Read() (LayerMetadata, error) {
	tomlPath := l.Path() + ".toml"
	layerMetadataFile, err := DecodeLayerMetadataFile(tomlPath, l.api, l.logger)
	if err != nil {
		return LayerMetadata{}, err
	}
	var sha string
	shaBytes, err := os.ReadFile(l.Path() + ".sha")
	if err != nil && !os.IsNotExist(err) { // if the sha file doesn't exist, an empty sha will be returned
		return LayerMetadata{}, err
	}
	if err == nil {
		sha = string(shaBytes)
	}
	return LayerMetadata{SHA: sha, LayerMetadataFile: layerMetadataFile}, nil
}

func (l *Layer) Remove() error {
	if err := os.RemoveAll(l.path); err != nil && !os.IsNotExist(err) {
		return err
	}
	if err := os.Remove(l.path + ".sha"); err != nil && !os.IsNotExist(err) {
		return err
	}
	if err := os.Remove(l.path + ".toml"); err != nil && !os.IsNotExist(err) {
		return err
	}
	return nil
}

func (l *Layer) WriteMetadata(metadata LayerMetadataFile) error {
	path := l.path + ".toml"
	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
		return err
	}
	return EncodeLayerMetadataFile(metadata, path, l.api)
}

func (l *Layer) WriteSha(sha string) error {
	if err := os.WriteFile(l.path+".sha", []byte(sha), 0666); err != nil {
		return err
	} // #nosec G306
	return nil
}
