package main

import (
	"errors"
	"io"
	"time"

	ipfsapi "github.com/ipfs/go-ipfs-api"
	"golang.org/x/net/context"
)

type File struct {
	Name         string    `json:"name"`
	AbsolutePath string    `json:"absolute_path"`
	Contents     io.Reader `json:"-"`
}

type Filestore interface {
	Get(name string) (*File, error)
	Set(file *File) error
	Delete(file *File) error
	Exists(name string) (bool, error)
}

func NewIPFSFilestore(ipfs *ipfsapi.Shell, rootDir string) (*IPFSFilestore, error) {
	store := &IPFSFilestore{
		ipfs:    ipfs,
		rootDir: rootDir,
	}

	exists, err := store.Exists("")
	if err != nil {
		return nil, err
	}

	if !exists {
		err := ipfs.FilesMkdir(newContext(), rootDir)
		if err != nil {
			return nil, err
		}
	}

	return store, nil
}

type IPFSFilestore struct {
	ipfs    *ipfsapi.Shell
	rootDir string
}

var ErrFileNotFound = errors.New("File not found")

type ErrFailedFileExistsCheck struct {
	parent error
}

func (e *ErrFailedFileExistsCheck) Unwrap() error {
	return e.parent
}

func (e *ErrFailedFileExistsCheck) Error() string {
	msg := "Failed to determine whether file exists"

	if e.parent != nil {
		msg += ": " + e.parent.Error()
	}

	return msg
}

func newContext() context.Context {
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Minute)
	return ctx
}

func (s *IPFSFilestore) Exists(name string) (bool, error) {
	ctx := newContext()
	_, err := s.ipfs.FilesStat(ctx, s.rootDir+"/"+name)
	if err != nil {
		if err.Error() == "files/stat: file does not exist" {
			return false, nil
		}

		return false, &ErrFailedFileExistsCheck{parent: err}
	}

	return true, nil
}

type FileReader struct {
	ipfs   *ipfsapi.Shell
	file   *File
	reader io.ReadCloser
}

func (r *FileReader) Read(p []byte) (int, error) {
	if r.reader == nil {
		var err error
		r.reader, err = r.ipfs.FilesRead(newContext(), r.file.AbsolutePath)
		if err != nil {
			return 0, err
		}
	}

	return r.reader.Read(p)
}

func (r *FileReader) Close() error {
	if r.reader == nil {
		return nil
	}

	return r.reader.Close()
}

func (s *IPFSFilestore) Get(name string) (*File, error) {
	exists, err := s.Exists(name)
	if err != nil {
		return nil, err
	}

	if !exists {
		return nil, ErrFileNotFound
	}

	file := &File{
		Name:         name,
		AbsolutePath: s.rootDir + "/" + name,
	}

	file.Contents = &FileReader{
		ipfs: s.ipfs,
		file: file,
	}

	return file, nil
}

func (s *IPFSFilestore) Set(file *File) error {
	return s.ipfs.FilesWrite(newContext(), s.rootDir+"/"+file.Name, file.Contents)
}

func (s *IPFSFilestore) Delete(file *File) error {
	return s.ipfs.FilesRm(newContext(), file.AbsolutePath, true)
}
