package bkfs

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"path"

	gwclient "github.com/moby/buildkit/frontend/gateway/client"
	"github.com/tonistiigi/fsutil/types"

	"github.com/tonistiigi/fsutil"

	"github.com/moby/buildkit/client/llb"
)

var (
	_ fs.DirEntry  = (*stateRefDirEntry)(nil)
	_ fs.ReadDirFS = (*StateRefFS)(nil)
	_ io.ReaderAt  = (*stateRefFile)(nil)
	_ fs.ReadDirFS = (*nullFS)(nil)
)

type StateRefFS struct {
	ctx context.Context
	ref gwclient.Reference
}

func FromRef(ctx context.Context, ref gwclient.Reference) *StateRefFS {
	return &StateRefFS{
		ctx: ctx,
		ref: ref,
	}
}

func FromState(ctx context.Context, state *llb.State, client gwclient.Client, opts ...llb.ConstraintsOpt) (fs.ReadDirFS, error) {
	if state == nil {
		return &nullFS{}, nil
	}

	res, err := fetchRef(client, *state, ctx, opts...)
	if err != nil {
		return nil, err
	}

	ref, err := res.SingleRef()
	if err != nil {
		return nil, err
	}

	return FromRef(ctx, ref), nil
}

func fetchRef(client gwclient.Client, st llb.State, ctx context.Context, opts ...llb.ConstraintsOpt) (*gwclient.Result, error) {
	def, err := st.Marshal(ctx, opts...)
	if err != nil {
		return nil, err
	}

	res, err := client.Solve(ctx, gwclient.SolveRequest{
		Definition: def.ToPB(),
	})
	if err != nil {
		return nil, err
	}

	return res, nil
}

type stateRefDirEntry struct {
	stat *types.Stat
}

func (s *stateRefDirEntry) Name() string {
	return path.Base(s.stat.Path)
}

func (s *stateRefDirEntry) IsDir() bool {
	return s.stat.IsDir()
}

func (s *stateRefDirEntry) Type() fs.FileMode {
	return fs.FileMode(s.stat.Mode)
}

func (s *stateRefDirEntry) Info() (fs.FileInfo, error) {
	info := &fsutil.StatInfo{
		Stat: s.stat,
	}

	return info, nil
}

func (st *StateRefFS) ReadDir(name string) ([]fs.DirEntry, error) {
	contents, err := st.ref.ReadDir(st.ctx, gwclient.ReadDirRequest{
		Path: name,
	})
	if err != nil {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
	}

	entries := []fs.DirEntry{}
	for _, stat := range contents {
		dirEntry := &stateRefDirEntry{stat: stat}
		entries = append(entries, dirEntry)
	}

	return entries, nil
}

type stateRefFile struct {
	eof    bool   // has file been read to EOF?
	path   string // the full path of the file from root
	ref    gwclient.Reference
	ctx    context.Context
	stat   *types.Stat
	offset int64
}

// close is a no-op
func (s *stateRefFile) Close() error {
	return nil
}

func (s *stateRefFile) ReadAt(b []byte, off int64) (int, error) {
	if off < 0 {
		return 0, &fs.PathError{Op: "read", Path: s.path, Err: fs.ErrInvalid}
	}

	if off >= s.stat.Size {
		return 0, io.EOF
	}

	segmentContents, err := s.ref.ReadFile(s.ctx, gwclient.ReadRequest{
		Filename: s.path,
		Range:    &gwclient.FileRange{Offset: int(off), Length: len(b)},
	})
	if err != nil {
		return 0, err
	}

	n := copy(b, segmentContents)

	// ReaderAt is supposed to return a descriptive error when the number of bytes read is less than
	// the length of the input buffer
	if n < len(b) {
		err = io.EOF
	}

	return n, err
}

// invariant: s.offset is the offset of the next byte to be read
func (s *stateRefFile) Read(b []byte) (int, error) {
	n, err := s.ReadAt(b, s.offset)
	s.offset += int64(n)

	return n, err
}

func (s *stateRefFile) Stat() (fs.FileInfo, error) {
	info := &fsutil.StatInfo{
		Stat: s.stat,
	}

	return info, nil
}

func (st *StateRefFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Err: fs.ErrInvalid, Path: name, Op: "open"}
	}

	stat, err := st.ref.StatFile(st.ctx, gwclient.StatRequest{
		Path: name,
	})
	if err != nil {
		return nil, &fs.PathError{Err: err, Op: "open", Path: name}
	}

	f := &stateRefFile{
		path:   name,
		ref:    st.ref,
		stat:   stat,
		ctx:    st.ctx,
		eof:    false,
		offset: 0,
	}

	return f, nil
}

type nullFS struct{}

func (st *nullFS) Open(name string) (fs.File, error) {
	return nil, fmt.Errorf("nullfs: %s: %w", name, fs.ErrNotExist)
}

func (st *nullFS) ReadDir(name string) ([]fs.DirEntry, error) {
	return nil, fmt.Errorf("nullfs: %s: %w", name, fs.ErrNotExist)
}
