package filestore

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path"

	"github.com/influxdata/influxdb/v2/chronograf"
	"github.com/influxdata/influxdb/v2/pkg/fs"
)

// AppExt is the the file extension searched for in the directory for layout files
const AppExt = ".json"

// Apps are canned JSON layouts.  Implements LayoutsStore.
type Apps struct {
	Dir      string                                      // Dir is the directory contained the pre-canned applications.
	Load     func(string) (chronograf.Layout, error)     // Load loads string name and return a Layout
	Filename func(string, chronograf.Layout) string      // Filename takes dir and layout and returns loadable file
	Create   func(string, chronograf.Layout) error       // Create will write layout to file.
	ReadDir  func(dirname string) ([]os.FileInfo, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
	Remove   func(name string) error                     // Remove file
	IDs      chronograf.ID                               // IDs generate unique ids for new application layouts
	Logger   chronograf.Logger
}

// NewApps constructs a layout store wrapping a file system directory
func NewApps(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.LayoutsStore {
	return &Apps{
		Dir:      dir,
		Load:     loadFile,
		Filename: fileName,
		Create:   createLayout,
		ReadDir:  ioutil.ReadDir,
		Remove:   os.Remove,
		IDs:      ids,
		Logger:   logger,
	}
}

func fileName(dir string, layout chronograf.Layout) string {
	base := fmt.Sprintf("%s%s", layout.Measurement, AppExt)
	return path.Join(dir, base)
}

func loadFile(name string) (chronograf.Layout, error) {
	octets, err := ioutil.ReadFile(name)
	if err != nil {
		return chronograf.Layout{}, chronograf.ErrLayoutNotFound
	}
	var layout chronograf.Layout
	if err = json.Unmarshal(octets, &layout); err != nil {
		return chronograf.Layout{}, chronograf.ErrLayoutInvalid
	}
	return layout, nil
}

func createLayout(file string, layout chronograf.Layout) error {
	h, err := fs.CreateFile(file)
	if err != nil {
		return err
	}
	defer h.Close()
	if octets, err := json.MarshalIndent(layout, "    ", "    "); err != nil {
		return chronograf.ErrLayoutInvalid
	} else if _, err := h.Write(octets); err != nil {
		return err
	}

	return nil
}

// All returns all layouts from the directory
func (a *Apps) All(ctx context.Context) ([]chronograf.Layout, error) {
	files, err := a.ReadDir(a.Dir)
	if err != nil {
		return nil, err
	}

	layouts := []chronograf.Layout{}
	for _, file := range files {
		if path.Ext(file.Name()) != AppExt {
			continue
		}
		if layout, err := a.Load(path.Join(a.Dir, file.Name())); err != nil {
			continue // We want to load all files we can.
		} else {
			layouts = append(layouts, layout)
		}
	}
	return layouts, nil
}

// Add creates a new layout within the directory
func (a *Apps) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
	var err error
	layout.ID, err = a.IDs.Generate()
	if err != nil {
		a.Logger.
			WithField("component", "apps").
			Error("Unable to generate ID")
		return chronograf.Layout{}, err
	}
	file := a.Filename(a.Dir, layout)
	if err = a.Create(file, layout); err != nil {
		if err == chronograf.ErrLayoutInvalid {
			a.Logger.
				WithField("component", "apps").
				WithField("name", file).
				Error("Invalid Layout: ", err)
		} else {
			a.Logger.
				WithField("component", "apps").
				WithField("name", file).
				Error("Unable to write layout:", err)
		}
		return chronograf.Layout{}, err
	}
	return layout, nil
}

// Delete removes a layout file from the directory
func (a *Apps) Delete(ctx context.Context, layout chronograf.Layout) error {
	_, file, err := a.idToFile(layout.ID)
	if err != nil {
		return err
	}

	if err := a.Remove(file); err != nil {
		a.Logger.
			WithField("component", "apps").
			WithField("name", file).
			Error("Unable to remove layout:", err)
		return err
	}
	return nil
}

// Get returns an app file from the layout directory
func (a *Apps) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
	l, file, err := a.idToFile(ID)
	if err != nil {
		return chronograf.Layout{}, err
	}

	if err != nil {
		if err == chronograf.ErrLayoutNotFound {
			a.Logger.
				WithField("component", "apps").
				WithField("name", file).
				Error("Unable to read file")
		} else if err == chronograf.ErrLayoutInvalid {
			a.Logger.
				WithField("component", "apps").
				WithField("name", file).
				Error("File is not a layout")
		}
		return chronograf.Layout{}, err
	}
	return l, nil
}

// Update replaces a layout from the file system directory
func (a *Apps) Update(ctx context.Context, layout chronograf.Layout) error {
	l, _, err := a.idToFile(layout.ID)
	if err != nil {
		return err
	}

	if err := a.Delete(ctx, l); err != nil {
		return err
	}
	file := a.Filename(a.Dir, layout)
	return a.Create(file, layout)
}

// idToFile takes an id and finds the associated filename
func (a *Apps) idToFile(ID string) (chronograf.Layout, string, error) {
	// Because the entire layout information is not known at this point, we need
	// to try to find the name of the file through matching the ID in the layout
	// content with the ID passed.
	files, err := a.ReadDir(a.Dir)
	if err != nil {
		return chronograf.Layout{}, "", err
	}

	for _, f := range files {
		if path.Ext(f.Name()) != AppExt {
			continue
		}
		file := path.Join(a.Dir, f.Name())
		layout, err := a.Load(file)
		if err != nil {
			return chronograf.Layout{}, "", err
		}
		if layout.ID == ID {
			return layout, file, nil
		}
	}

	return chronograf.Layout{}, "", chronograf.ErrLayoutNotFound
}
