package artifact

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

	"go.sour.is/paste/src/pkg/readutil"
	"golang.org/x/sys/unix"
	"sour.is/x/toolbox/httpsrv"
	"sour.is/x/toolbox/log"

	"github.com/gomarkdown/markdown"
	"github.com/gomarkdown/markdown/html"
	"github.com/gomarkdown/markdown/parser"
)

// artifact stores items to disk
type artifact struct {
	store   string
	maxSize int64
}

const DefaultMaxSize = 500 * 1024 * 1024

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

	if maxSize > 0 {
		a.maxSize = maxSize
	}

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

	return a, nil
}

func (a *artifact) RegisterHTTP(mux *http.ServeMux) {
	mux.Handle("/a", http.StripPrefix("/a", a))
	mux.Handle("/a/", http.StripPrefix("/a/", a))
}
func (a *artifact) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		if r.URL.Path == "" {
			a.list(w)
			return
		}

		name := strings.TrimPrefix(r.URL.Path, "/")
		name, path, _ := strings.Cut(name, "/")

		a.get(w, name, path)
	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(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)
		}

		fmt.Fprintf(w, "OK %s", id)

	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}
func (a *artifact) get(w http.ResponseWriter, name string, path string) error {
	hasPath := path != ""

	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()

	if !hasPath {
		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
	}

	rdr, err := readutil.Decompress(f)
	if err != nil && err != readutil.ErrUnsupportedType {
		return fmt.Errorf("%w: %w", ErrReadingContent, err)
	}
	if cl, ok := rdr.(io.ReadCloser); ok {
		defer cl.Close()
	}

	tr := tar.NewReader(rdr)
	if path == "@" {
		var paths []string
		for {
			hdr, err := tr.Next()
			if err == io.EOF {
				break // End of archive
			}
			if err != nil {
				log.Error(err)
			}
			paths = append(paths, hdr.Name)
		}

		httpsrv.WriteObject(w, http.StatusOK, paths)
		return nil
	}

	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break // End of archive
		}
		if err != nil {
			log.Error(err)
			break
		}
		if path == "~" && hdr.Name == "index.html" {
			path = hdr.Name
		}
		if path == "~" && hdr.Name == "index.md" {
			path = hdr.Name
		}

		if hdr.Name == path {
			if strings.HasSuffix(hdr.Name, ".md") {
				md, err := io.ReadAll(tr)
				if err != nil {
					return fmt.Errorf("%w: %w", ErrReadingContent, err)
				}

				w.Header().Set("Content-Type", "text/html")
				extensions := parser.CommonExtensions | parser.AutoHeadingIDs
				p := parser.NewWithExtensions(extensions)

				htmlFlags := html.CommonFlags | html.HrefTargetBlank
				opts := html.RendererOptions{Flags: htmlFlags}
				renderer := html.NewRenderer(opts)

				b := markdown.ToHTML(md, p, renderer)
				_, _ = w.Write(b)
				return nil
			}

			pr := readutil.NewPreviewReader(tr)

			mime, err := readutil.ReadMIME(pr, hdr.Name)
			if err != nil {
				return fmt.Errorf("%w: %w", ErrReadingContent, err)
			}
			w.Header().Set("Content-Type", mime)

			if _, err := io.Copy(w, pr.Drain()); err != nil {
				log.Error(err)
			}
			return nil
		}
	}

	http.Error(w, "Not Found in Archive", http.StatusNotFound)
	return ErrNotFound
}
func (a *artifact) put(r io.ReadCloser, length int) (string, error) {
	defer r.Close()

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

	rdr := io.LimitReader(r, a.maxSize)
	pr := readutil.NewPreviewReader(rdr)
	rdr = pr.Drain()

	s256 := sha256.New()
	tmp, err := os.CreateTemp(a.store, "arch-")
	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)

	// log.Debugs("Artifact: moving file", "src", tmp.Name(), "dst", fname)
	_ = os.Rename(tmp.Name(), fname)

	return id, nil
}
func (a *artifact) list(w io.Writer) error {
	return filepath.Walk(a.store, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}

		_, err = fmt.Fprintln(w, "FILE: ", info.Name())
		return err
	})
}

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")
)
