package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"git.sr.ht/~emersion/gqlclient"
	"github.com/dustin/go-humanize"
	"github.com/spf13/cobra"

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

func newPasteCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "paste",
		Short: "Use the paste API",
	}
	cmd.AddCommand(newPasteCreateCommand())
	cmd.AddCommand(newPasteDeleteCommand())
	cmd.AddCommand(newPasteListCommand())
	cmd.AddCommand(newPasteShowCommand())
	cmd.AddCommand(newPasteUpdateCommand())
	cmd.AddCommand(newPasteUserWebhookCommand())
	return cmd
}

func newPasteCreateCommand() *cobra.Command {
	var visibility string
	var name string
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		pasteVisibility, err := pastesrht.ParseVisibility(visibility)
		if err != nil {
			log.Fatal(err)
		}

		c := createClient("paste", cmd)

		if name != "" && len(args) > 0 {
			log.Fatalln("--name is only supported when reading from stdin")
		}

		var files []gqlclient.Upload
		for _, filename := range args {
			f, err := os.Open(filename)
			if err != nil {
				log.Fatalf("failed to open input file: %v", err)
			}
			defer f.Close()

			t := mime.TypeByExtension(filename)
			if t == "" {
				t = "text/plain"
			}

			files = append(files, gqlclient.Upload{
				Filename: filepath.Base(filename),
				MIMEType: t,
				Body:     f,
			})
		}

		if len(args) == 0 {
			files = append(files, gqlclient.Upload{
				Filename: name,
				MIMEType: "text/plain",
				Body:     os.Stdin,
			})
		}

		paste, err := pastesrht.CreatePaste(c.Client, ctx, files, pasteVisibility)
		if err != nil {
			log.Fatal(err)
		}

		if termfmt.IsTerminal() {
			log.Printf("Created paste %v/%v/%v", c.BaseURL, paste.User.CanonicalName, paste.Id)
		} else {
			fmt.Printf("%v/%v/%v\n", c.BaseURL, paste.User.CanonicalName, paste.Id)
		}
	}

	cmd := &cobra.Command{
		Use:   "create [filenames...]",
		Short: "Create a new paste",
		Run:   run,
	}
	cmd.Flags().StringVarP(&visibility, "visibility", "v", "unlisted", "paste visibility")
	cmd.RegisterFlagCompletionFunc("visibility", completeVisibility)
	cmd.Flags().StringVarP(&name, "name", "n", "", "paste name (when reading from stdin)")
	return cmd
}

func newPasteDeleteCommand() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		for _, arg := range args {
			id, _, instance := parseResourceName(arg)
			c := createClientWithInstance("paste", cmd, instance)

			paste, err := pastesrht.Delete(c.Client, ctx, id)
			if err != nil {
				log.Fatalf("failed to delete paste %s: %v", id, err)
			}

			if paste == nil {
				log.Printf("Paste %s does not exist\n", id)
			} else {
				log.Printf("Deleted paste %s\n", paste.Id)
			}
		}
	}

	cmd := &cobra.Command{
		Use:               "delete <ID...>",
		Short:             "Delete pastes",
		Args:              cobra.MinimumNArgs(1),
		ValidArgsFunction: completePasteID,
		Run:               run,
	}
	return cmd
}

func newPasteListCommand() *cobra.Command {
	var count int
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		c := createClient("paste", cmd)
		var cursor *pastesrht.Cursor

		err := pagerify(func(p pager) error {
			pastes, err := pastesrht.Pastes(c.Client, ctx, cursor)
			if err != nil {
				return err
			}

			for _, paste := range pastes.Results {
				printPaste(p, &paste)
				fmt.Fprintln(p)
			}

			cursor = pastes.Cursor
			if p.IsDone(cursor, len(pastes.Results)) {
				return pagerDone
			}

			return nil
		}, count)
		if err != nil {
			log.Fatal(err)
		}

	}

	cmd := &cobra.Command{
		Use:   "list",
		Short: "List pastes",
		Run:   run,
	}
	cmd.Flags().IntVar(&count, "count", 0, "number of pastes to fetch")
	cmd.RegisterFlagCompletionFunc("count", cobra.NoFileCompletions)
	return cmd
}

func printPaste(w io.Writer, paste *pastesrht.Paste) {
	fmt.Fprintf(w, "%s %s %s\n", termfmt.DarkYellow.Sprint(paste.Id),
		paste.Visibility.TermString(), humanize.Time(paste.Created.Time))
	for _, file := range paste.Files {
		if file.Filename != nil && *file.Filename != "" {
			fmt.Fprintf(w, "  %s\n", *file.Filename)
		}
	}
}

func newPasteShowCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:               "show <id>",
		Short:             "Display a paste",
		Args:              cobra.ExactArgs(1),
		ValidArgsFunction: completePasteID,
	}
	cmd.Run = func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		id, _, instance := parseResourceName(args[0])
		c := createClientWithInstance("paste", cmd, instance)

		paste, err := pastesrht.ShowPaste(c.Client, ctx, id)
		if err != nil {
			log.Fatal(err)
		} else if paste == nil {
			log.Fatalf("Paste %q does not exist", id)
		}

		fmt.Printf("%s %s %s\n", termfmt.DarkYellow.Sprint(paste.Id),
			paste.Visibility.TermString(), humanize.Time(paste.Created.Time))

		for _, file := range paste.Files {
			fmt.Print("\n■ ")
			if file.Filename != nil && *file.Filename != "" {
				fmt.Println(termfmt.Bold.String(*file.Filename))
			} else {
				fmt.Println(termfmt.Dim.String("(untitled)"))
			}
			fmt.Println()

			fetchPasteFile(ctx, c.HTTP, &file)
		}
	}
	return cmd
}

func fetchPasteFile(ctx context.Context, c *http.Client, file *pastesrht.File) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil)
	if err != nil {
		log.Fatalf("Failed to create request to fetch file: %v", err)
	}

	resp, err := c.Do(req)
	if err != nil {
		log.Fatalf("Failed to fetch file: %v", err)
	}
	defer resp.Body.Close()

	if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
		log.Fatalf("Failed to copy to stdout: %v", err)
	}
}

func newPasteUpdateCommand() *cobra.Command {
	var visibility string
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		c := createClient("paste", cmd)

		pasteVisibility, err := pastesrht.ParseVisibility(visibility)
		if err != nil {
			log.Fatal(err)
		}

		paste, err := pastesrht.Update(c.Client, ctx, args[0], pasteVisibility)
		if err != nil {
			log.Fatal(err)
		}

		if paste == nil {
			log.Fatalf("Paste %s does not exist\n", args[0])
		}

		log.Printf("Updated paste %s visibility to %s\n", paste.Id, pasteVisibility)
	}

	cmd := &cobra.Command{
		Use:               "update <ID>",
		Short:             "Update a paste's visibility",
		Args:              cobra.ExactArgs(1),
		ValidArgsFunction: completePasteID,
		Run:               run,
	}
	cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "paste visibility")
	cmd.MarkFlagRequired("visibility")
	cmd.RegisterFlagCompletionFunc("visibility", completeVisibility)
	return cmd
}

func newPasteUserWebhookCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "user-webhook",
		Short: "Manage user webhooks",
	}
	cmd.AddCommand(newPasteUserWebhookCreateCommand())
	cmd.AddCommand(newPasteUserWebhookListCommand())
	cmd.AddCommand(newPasteUserWebhookDeleteCommand())
	return cmd
}

func newPasteUserWebhookCreateCommand() *cobra.Command {
	var events []string
	var stdin bool
	var url string
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		c := createClient("paste", cmd)

		var config pastesrht.UserWebhookInput
		config.Url = url

		whEvents, err := pastesrht.ParseEvents(events)
		if err != nil {
			log.Fatal(err)
		}
		config.Events = whEvents
		config.Query = readWebhookQuery(stdin)

		webhook, err := pastesrht.CreateUserWebhook(c.Client, ctx, config)
		if err != nil {
			log.Fatal(err)
		}

		log.Printf("Created user webhook with ID %d\n", webhook.Id)
	}

	cmd := &cobra.Command{
		Use:               "create",
		Short:             "Create a user webhook",
		Args:              cobra.ExactArgs(0),
		ValidArgsFunction: cobra.NoFileCompletions,
		Run:               run,
	}
	cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events")
	cmd.RegisterFlagCompletionFunc("events", completePasteUserWebhookEvents)
	cmd.MarkFlagRequired("events")
	cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin")
	cmd.Flags().StringVarP(&url, "url", "u", "", "payload url")
	cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions)
	cmd.MarkFlagRequired("url")
	return cmd
}

func newPasteUserWebhookListCommand() *cobra.Command {
	var count int
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		c := createClient("paste", cmd)
		var cursor *pastesrht.Cursor

		err := pagerify(func(p pager) error {
			webhooks, err := pastesrht.UserWebhooks(c.Client, ctx, cursor)
			if err != nil {
				return err
			}

			for _, webhook := range webhooks.Results {
				fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
			}

			cursor = webhooks.Cursor
			if p.IsDone(cursor, len(webhooks.Results)) {
				return pagerDone
			}

			return nil
		}, count)
		if err != nil {
			log.Fatal(err)
		}
	}

	cmd := &cobra.Command{
		Use:   "list",
		Short: "List user webhooks",
		Args:  cobra.ExactArgs(0),
		Run:   run,
	}
	cmd.Flags().IntVar(&count, "count", 0, "number of webhooks to fetch")
	cmd.RegisterFlagCompletionFunc("count", cobra.NoFileCompletions)
	return cmd
}

func newPasteUserWebhookDeleteCommand() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()
		c := createClient("paste", cmd)

		id, err := parseInt32(args[0])
		if err != nil {
			log.Fatal(err)
		}

		webhook, err := pastesrht.DeleteUserWebhook(c.Client, ctx, id)
		if err != nil {
			log.Fatal(err)
		}

		log.Printf("Deleted webhook %d\n", webhook.Id)
	}

	cmd := &cobra.Command{
		Use:               "delete <ID>",
		Short:             "Delete a user webhook",
		Args:              cobra.ExactArgs(1),
		ValidArgsFunction: completePasteUserWebhookID,
		Run:               run,
	}
	return cmd
}

func completePasteID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	ctx := cmd.Context()
	c := createClient("paste", cmd)
	var pasteList []string

	pastes, err := pastesrht.PasteCompletionList(c.Client, ctx)
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}

	for _, paste := range pastes.Results {
		if cmd.Name() == "delete" && hasCmdArg(cmd, paste.Id) {
			continue
		}

		str := paste.Id
		var files string

		for i, file := range paste.Files {
			if file.Filename != nil && *file.Filename != "" {
				if i != 0 {
					files += ", "
				}
				files += *file.Filename
			}
		}

		if files != "" {
			str += fmt.Sprintf("\t%s", files)
		}

		pasteList = append(pasteList, str)
	}

	return pasteList, cobra.ShellCompDirectiveNoFileComp
}

func completePasteUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	var eventList []string
	events := [3]string{"paste_created", "paste_updated", "paste_deleted"}
	set := strings.ToLower(cmd.Flag("events").Value.String())
	for _, event := range events {
		if !strings.Contains(set, event) {
			eventList = append(eventList, event)
		}
	}
	return eventList, cobra.ShellCompDirectiveNoFileComp
}

func completePasteUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	ctx := cmd.Context()
	c := createClient("paste", cmd)
	var webhookList []string

	webhooks, err := pastesrht.UserWebhooks(c.Client, ctx, nil)
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}

	for _, webhook := range webhooks.Results {
		s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url)
		webhookList = append(webhookList, s)
	}

	return webhookList, cobra.ShellCompDirectiveNoFileComp
}
