package export

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
	"time"

	"git.sr.ht/~emersion/gqlclient"

	"git.sr.ht/~xenrox/hut/srht/pastesrht"
)

const pasteFilesDir = "files"

type PasteExporter struct {
	client *gqlclient.Client
	http   *http.Client
}

func NewPasteExporter(client *gqlclient.Client, http *http.Client) *PasteExporter {
	// XXX: Is this a sane default?
	newHttp := *http
	newHttp.Timeout = 10 * time.Minute
	return &PasteExporter{
		client: client,
		http:   &newHttp,
	}
}

type PasteInfo struct {
	Info
	Visibility pastesrht.Visibility `json:"visibility"`
}

func (ex *PasteExporter) Export(ctx context.Context, dir string) error {
	var cursor *pastesrht.Cursor
	var ret error

	for {
		pastes, err := pastesrht.PasteContents(ex.client, ctx, cursor)
		if err != nil {
			return err
		}

		for _, paste := range pastes.Results {
			base := path.Join(dir, paste.Id)
			if err := ex.exportPaste(ctx, &paste, base); err != nil {
				var pe partialError
				if errors.As(err, &pe) {
					ret = err
					continue
				}
				return err
			}
		}

		cursor = pastes.Cursor
		if cursor == nil {
			break
		}
	}

	return ret
}

func (ex *PasteExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	paste, err := pastesrht.PasteContentsByID(ex.client, ctx, resource)
	if err != nil {
		return err
	}
	return ex.exportPaste(ctx, paste, dir)
}

func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, base string) error {
	infoPath := path.Join(base, infoFilename)
	if _, err := os.Stat(infoPath); err == nil {
		log.Printf("\tSkipping %s (already exists)", paste.Id)
		return nil
	}

	log.Printf("\t%s", paste.Id)
	files := path.Join(base, pasteFilesDir)
	if err := os.MkdirAll(files, 0o755); err != nil {
		return err
	}

	var ret error
	for _, file := range paste.Files {
		if err := ex.exportFile(ctx, paste, files, &file); err != nil {
			ret = err
		}
	}

	pasteInfo := PasteInfo{
		Info: Info{
			Service: "paste.sr.ht",
			Name:    paste.Id,
		},
		Visibility: paste.Visibility,
	}
	if err := writeJSON(infoPath, &pasteInfo); err != nil {
		return err
	}

	return ret
}

func (ex *PasteExporter) exportFile(ctx context.Context, paste *pastesrht.Paste, base string, file *pastesrht.File) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil)
	if err != nil {
		return err
	}
	resp, err := ex.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	name := paste.Id
	if file.Filename != nil && *file.Filename != "" {
		name = *file.Filename
	}

	if resp.StatusCode != http.StatusOK {
		return partialError{fmt.Errorf("%s/%s: server returned non-200 status %d", paste.Id, name, resp.StatusCode)}
	}

	f, err := os.Create(path.Join(base, name))
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = io.Copy(f, resp.Body)
	if err != nil {
		return err
	}

	return nil
}

func (ex *PasteExporter) ImportResource(ctx context.Context, dir string) error {
	var info PasteInfo
	if err := readJSON(path.Join(dir, infoFilename), &info); err != nil {
		return err
	}

	return ex.importPaste(ctx, &info, dir)
}

func (ex *PasteExporter) importPaste(ctx context.Context, paste *PasteInfo, base string) error {
	filesPath := path.Join(base, pasteFilesDir)
	items, err := os.ReadDir(filesPath)
	if err != nil {
		return err
	}

	var files []gqlclient.Upload
	for _, item := range items {
		if item.IsDir() {
			continue
		}

		f, err := os.Open(path.Join(filesPath, item.Name()))
		if err != nil {
			return err
		}
		defer f.Close()

		var name string
		if item.Name() != paste.Name {
			name = item.Name()
		}

		files = append(files, gqlclient.Upload{
			Filename: name,
			// MIMEType is not used by the API, except for checking that it is a "text".
			// Parsing the MIME type from the extension would cause issues: ".json" is parsed as "application/json",
			// which gets rejected because it is not a "text/".
			// Since the API does not use the type besides that, always send a dummy text value.
			MIMEType: "text/plain",
			Body:     f,
		})
	}

	if _, err := pastesrht.CreatePaste(ex.client, ctx, files, paste.Visibility); err != nil {
		return fmt.Errorf("failed to create paste: %v", err)
	}
	return nil
}
