package image

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/h2non/filetype"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"golang.org/x/sys/unix"

	"go.sour.is/paste/src/pkg/readutil"
	"go.sour.is/pkg/lg"
)

type image struct {
	store   string
	maxSize int64
}

const DefaultMaxSize = 500 * 1024 * 1024

func New(store string, maxSize int64) (a *image, err error) {
	a = &image{
		store:   store,
		maxSize: DefaultMaxSize,
	}

	if maxSize > 0 {
		a.maxSize = maxSize
	}

	if !chkStore(a.store) {
		return nil, fmt.Errorf("image Store location [%s] does not exist or is not writable", a.store)
	}

	return a, nil
}
func (a *image) RegisterHTTP(mux *http.ServeMux) {
	mux.Handle("/i", http.StripPrefix("/i", a))
	mux.Handle("/i/", http.StripPrefix("/i/", a))
	mux.Handle("3/upload", http.StripPrefix("/3/upload", a))

}
func (a *image) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	ctx, span := lg.Span(ctx)
	defer span.End()

	switch r.Method {
	case http.MethodGet:
		name := strings.TrimPrefix(r.URL.Path, "/")

		a.get(ctx, w, name)
	case http.MethodPost:
		length := 0
		if h := r.Header.Get("Content-Length"); h != "" {
			if i, err := strconv.Atoi(h); err != nil {
				length = i
			}
		}
		id, err := a.put(ctx, w, r.Body, length)
		switch {
		case errors.Is(err, ErrGone):
			w.WriteHeader(http.StatusGone)
		case errors.Is(err, ErrNotFound):
			w.WriteHeader(http.StatusNotFound)
		case errors.Is(err, ErrReadingContent):
			w.WriteHeader(http.StatusInternalServerError)
		case errors.Is(err, ErrUnsupportedType):
			w.WriteHeader(http.StatusUnsupportedMediaType)
		}

		type data struct{
			Link string `json:"link"`
			DeleteHash string `json:"deletehash"`
		}
		var resp = struct{
			Data data `json:"data"`
			Success bool `json:"success"`
			Status int `json:"status"`
		}{
			Data: data{
				Link: fmt.Sprintf("https://%s/%s", r.Host, id),
			},
			Success: true,
			Status: 200,
		}

		json.NewEncoder(w).Encode(resp)

	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}

func (a *image) get(ctx context.Context, w http.ResponseWriter, name string) error {
	_, span := lg.Span(ctx)
	defer span.End()

	ext := filepath.Ext(name)
	id := strings.TrimSuffix(name, ext)

	fname := filepath.Join(a.store, id)

	if !chkFile(fname) {
		return fmt.Errorf("%w: %s", ErrNotFound, fname)
	}

	if chkGone(fname) {
		return fmt.Errorf("%w: %s", ErrGone, fname)
	}

	f, err := os.Open(fname)
	if err != nil {
		return err
	}
	defer f.Close()

	pr := readutil.NewPreviewReader(f)

	mime, err := readutil.ReadMIME(pr, name)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", mime)
	w.Header().Set("X-Content-Type-Options", "nosniff")

	_, _ = io.Copy(w, pr.Drain())
	return nil
}

func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser, length int) (string, error) {
	_, span := lg.Span(ctx)
	defer span.End()

	defer r.Close()

	if length > 0 {
		if int64(length) > a.maxSize {
			return "", ErrSizeTooLarge
		}
	}

	rdr := io.LimitReader(r, a.maxSize)
	pr := readutil.NewPreviewReader(rdr)
	if !isImageOrVideo(pr) {
		return "", ErrUnsupportedType
	}
	rdr = pr.Drain()

	s256 := sha256.New()
	tmp, err := os.CreateTemp(a.store, "image-")
	if err != nil {
		return "", fmt.Errorf("%w: %w", ErrBadInput, err)
	}

	defer os.Remove(tmp.Name())

	m := io.MultiWriter(s256, tmp)
	if _, err := io.Copy(m, rdr); err != nil {
		return "", fmt.Errorf("%w: %w", ErrBadInput, err)
	}
	tmp.Close()

	id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:])
	fname := filepath.Join(a.store, id)

	span.AddEvent("image: moving file", trace.WithAttributes(attribute.String("src", tmp.Name()), attribute.String("dst", fname)))
	_ = os.Rename(tmp.Name(), fname)

	return id, nil
}

func isImageOrVideo(in io.Reader) bool {
	buf := make([]byte, 320)
	_, err := in.Read(buf)
	if err != nil {
		return false
	}
	return filetype.IsImage(buf) || filetype.IsVideo(buf)
}

func chkStore(path string) bool {
	file, err := os.Stat(path)

	if err != nil && os.IsNotExist(err) {
		err = os.MkdirAll(path, 0744)
		if err != nil {
			return false
		}

		file, err = os.Stat(path)
	}

	if err != nil {
		return false
	}

	if !file.IsDir() {
		return false
	}

	if unix.Access(path, unix.W_OK&unix.R_OK) != nil {
		return false
	}

	return true
}
func chkFile(path string) bool {
	file, err := os.Stat(path)
	if err != nil {
		return false
	}

	if file.IsDir() {
		return false
	}

	if unix.Access(path, unix.W_OK&unix.R_OK) != nil {
		return false
	}

	return true
}
func chkGone(path string) bool {
	file, err := os.Stat(path)
	if err != nil {
		return true
	}

	if file.Size() == 0 {
		return true
	}

	return false
}

var (
	ErrNotFound        = errors.New("not found")
	ErrGone            = errors.New("gone")
	ErrReadingContent  = errors.New("reading content")
	ErrSizeTooLarge    = errors.New("size too large")
	ErrBadInput        = errors.New("bad input")
	ErrUnsupportedType = errors.New("unsupported type")
)
