package attachments

import (
	"bufio"
	"bytes"
	"errors"
	"io"
	"os"
	"strings"
	"sync/atomic"
	"time"

	"github.com/keybase/client/go/chat/globals"
	"github.com/keybase/client/go/libkb"
	"github.com/keybase/client/go/protocol/chat1"
	"github.com/keybase/client/go/protocol/gregor1"
	"github.com/keybase/client/go/protocol/keybase1"
	"github.com/keybase/go-framed-msgpack-rpc/rpc"
	"golang.org/x/net/context"
)

func AssetFromMessage(ctx context.Context, g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
	msgID chat1.MessageID, preview bool) (res chat1.Asset, err error) {
	reason := chat1.GetThreadReason_GENERAL
	msgs, err := g.ChatHelper.GetMessages(ctx, uid, convID, []chat1.MessageID{msgID}, true, &reason)
	if err != nil {
		return res, err
	}
	if len(msgs) == 0 {
		return res, libkb.NotFoundError{}
	}
	first := msgs[0]
	st, err := first.State()
	if err != nil {
		return res, err
	}
	if st == chat1.MessageUnboxedState_ERROR {
		em := first.Error().ErrMsg
		return res, errors.New(em)
	}
	if st != chat1.MessageUnboxedState_VALID {
		// given a message id that doesn't exist, msgs can come back
		// with an empty message in it (and st == 0).
		// this check prevents a panic, but perhaps the server needs
		// a fix as well.
		return res, libkb.NotFoundError{}
	}

	msg := first.Valid()
	body := msg.MessageBody
	t, err := body.MessageType()
	if err != nil {
		return res, err
	}

	var attachment chat1.MessageAttachment
	switch t {
	case chat1.MessageType_ATTACHMENT:
		attachment = msg.MessageBody.Attachment()
	case chat1.MessageType_ATTACHMENTUPLOADED:
		uploaded := msg.MessageBody.Attachmentuploaded()
		attachment = chat1.MessageAttachment{
			Object:   uploaded.Object,
			Previews: uploaded.Previews,
			Metadata: uploaded.Metadata,
		}
	default:
		return res, errors.New("not an attachment message")
	}
	res = attachment.Object
	if preview {
		if len(attachment.Previews) > 0 {
			res = attachment.Previews[0]
		} else if attachment.Preview != nil {
			res = *attachment.Preview
		} else {
			return res, errors.New("no preview in attachment")
		}
	}
	return res, nil
}

// ReadCloseResetter is io.ReadCloser plus a Reset method. This is used by
// attachment uploads.
type ReadCloseResetter interface {
	io.ReadCloser
	Reset() error
}

type FileReadCloseResetter struct {
	filename string
	file     *os.File
	buf      *bufio.Reader
}

// NewFileReadCloseResetter creates a ReadCloseResetter that uses an on-disk file as
// source of data.
func NewFileReadCloseResetter(name string) (ReadCloseResetter, error) {
	f := &FileReadCloseResetter{filename: name}
	if err := f.open(); err != nil {
		return nil, err
	}
	return f, nil
}

func (f *FileReadCloseResetter) open() error {
	ff, err := os.Open(f.filename)
	if err != nil {
		return err
	}
	f.file = ff
	f.buf = bufio.NewReader(f.file)
	return nil
}

func (f *FileReadCloseResetter) Read(p []byte) (int, error) {
	return f.buf.Read(p)
}

func (f *FileReadCloseResetter) Reset() error {
	_, err := f.file.Seek(0, io.SeekStart)
	if err != nil {
		return err
	}
	f.buf.Reset(f.file)
	return nil
}

func (f *FileReadCloseResetter) Close() error {
	f.buf = nil
	if f.file != nil {
		return f.file.Close()
	}
	return nil
}

// KbfsReadCloseResetter is an implementation of ReadCloseResetter that uses
// SimpleFS as source of data.
type KbfsReadCloseResetter struct {
	client *keybase1.SimpleFSClient
	opid   keybase1.OpID
	offset int64
	ctx    context.Context
}

const (
	kbfsPrefix        = "/keybase"
	kbfsPrefixPrivate = kbfsPrefix + "/private/"
	kbfsPrefixPublic  = kbfsPrefix + "/public/"
	kbfsPrefixTeam    = kbfsPrefix + "/team/"
)

func isKbfsPath(p string) bool {
	return strings.HasPrefix(p, kbfsPrefixPrivate) ||
		strings.HasPrefix(p, kbfsPrefixPublic) ||
		strings.HasPrefix(p, kbfsPrefixTeam)
}

func makeSimpleFSClientFromGlobalContext(
	g *libkb.GlobalContext) (*keybase1.SimpleFSClient, error) {
	xp := g.ConnectionManager.LookupByClientType(keybase1.ClientType_KBFS)
	if xp == nil {
		return nil, libkb.KBFSNotRunningError{}
	}
	return &keybase1.SimpleFSClient{
		Cli: rpc.NewClient(xp, libkb.NewContextifiedErrorUnwrapper(g), nil),
	}, nil
}

// NewKbfsReadCloseResetter creates a ReadCloseResetter that uses SimpleFS as source
// of data. kbfsPath must start with "/keybase/<tlf-type>/".
func NewKbfsReadCloseResetter(ctx context.Context, g *libkb.GlobalContext,
	kbfsPath string) (ReadCloseResetter, error) {
	if !isKbfsPath(kbfsPath) {
		return nil, errors.New("not a kbfs path")
	}

	client, err := makeSimpleFSClientFromGlobalContext(g)
	if err != nil {
		return nil, err
	}

	opid, err := client.SimpleFSMakeOpid(ctx)
	if err != nil {
		return nil, err
	}

	if err = client.SimpleFSOpen(ctx, keybase1.SimpleFSOpenArg{
		OpID:  opid,
		Dest:  keybase1.NewPathWithKbfsPath(kbfsPath[len(kbfsPrefix):]),
		Flags: keybase1.OpenFlags_READ | keybase1.OpenFlags_EXISTING,
	}); err != nil {
		return nil, err
	}

	return &KbfsReadCloseResetter{
		client: client,
		ctx:    ctx,
		opid:   opid,
	}, nil
}

// Read implements the ReadCloseResetter interface.
func (f *KbfsReadCloseResetter) Read(p []byte) (int, error) {
	content, err := f.client.SimpleFSRead(f.ctx, keybase1.SimpleFSReadArg{
		OpID:   f.opid,
		Offset: atomic.LoadInt64(&f.offset),
		Size:   len(p),
	})
	if err != nil {
		return 0, err
	}
	if len(content.Data) == 0 {
		// Unfortunately SimpleFSRead doesn't return EOF error.
		return 0, io.EOF
	}
	atomic.AddInt64(&f.offset, int64(len(content.Data)))
	copy(p, content.Data)
	return len(content.Data), nil
}

// Reset implements the ReadCloseResetter interface.
func (f *KbfsReadCloseResetter) Reset() error {
	atomic.StoreInt64(&f.offset, 0)
	return nil
}

// Close implements the ReadCloseResetter interface.
func (f *KbfsReadCloseResetter) Close() error {
	return f.client.SimpleFSClose(f.ctx, f.opid)
}

// NewReadCloseResetter creates a ReadCloseResetter using either on-disk file
// or SimpleFS depending on if p is a KBFS path.
func NewReadCloseResetter(ctx context.Context, g *libkb.GlobalContext,
	p string) (ReadCloseResetter, error) {
	if isKbfsPath(p) {
		return NewKbfsReadCloseResetter(ctx, g, p)
	}
	return NewFileReadCloseResetter(p)
}

type kbfsFileInfo struct {
	dirent *keybase1.Dirent
}

func (fi kbfsFileInfo) Name() string { return fi.dirent.Name }
func (fi kbfsFileInfo) Size() int64  { return int64(fi.dirent.Size) }
func (fi kbfsFileInfo) Mode() (mode os.FileMode) {
	mode |= 0400
	if fi.dirent.Writable {
		mode |= 0200
	}
	switch fi.dirent.DirentType {
	case keybase1.DirentType_DIR:
		mode |= os.ModeDir
	case keybase1.DirentType_SYM:
		mode |= os.ModeSymlink
	case keybase1.DirentType_EXEC:
		mode |= 0100
	}
	return mode
}
func (fi kbfsFileInfo) ModTime() time.Time {
	return keybase1.FromTime(fi.dirent.Time)
}
func (fi kbfsFileInfo) IsDir() bool {
	return fi.dirent.DirentType == keybase1.DirentType_DIR
}
func (fi kbfsFileInfo) Sys() interface{} { return fi.dirent }

// StatOSOrKbfsFile stats the file located at p, using SimpleFSStat if it's a
// KBFS path, or os.Stat if not.
func StatOSOrKbfsFile(ctx context.Context, g *libkb.GlobalContext, p string) (
	fi os.FileInfo, err error) {
	if !isKbfsPath(p) {
		return os.Stat(p)
	}

	client, err := makeSimpleFSClientFromGlobalContext(g)
	if err != nil {
		return nil, err
	}

	dirent, err := client.SimpleFSStat(ctx, keybase1.SimpleFSStatArg{
		Path: keybase1.NewPathWithKbfsPath(p[len(kbfsPrefix):]),
	})
	if err != nil {
		return nil, err
	}

	return kbfsFileInfo{dirent: &dirent}, nil
}

type BufReadResetter struct {
	buf []byte
	r   *bytes.Reader
}

func NewBufReadResetter(buf []byte) *BufReadResetter {
	return &BufReadResetter{
		buf: buf,
		r:   bytes.NewReader(buf),
	}
}

func (b *BufReadResetter) Read(p []byte) (int, error) {
	return b.r.Read(p)
}

func (b *BufReadResetter) Reset() error {
	b.r.Reset(b.buf)
	return nil
}

func AddPendingPreview(ctx context.Context, g *globals.Context, obr *chat1.OutboxRecord) error {
	pre, err := NewPendingPreviews(g).Get(ctx, obr.OutboxID)
	if err != nil {
		return err
	}
	mpr, err := pre.Export(func() *chat1.PreviewLocation {
		loc := chat1.NewPreviewLocationWithUrl(g.AttachmentURLSrv.GetPendingPreviewURL(ctx,
			obr.OutboxID))
		return &loc
	})
	if err != nil {
		return err
	}
	obr.Preview = &mpr
	return nil
}
