package main

import (
	"bytes"
	"context"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"os/signal"
	"runtime"
	"strings"
	"syscall"
	"time"

	"github.com/denisbrodbeck/striphtmltags"
	log "github.com/go-pkgz/lgr"
	flags "github.com/jessevdk/go-flags"

	"git.lattuga.net/boyska/rss2twitter/app/publisher"
	"git.lattuga.net/boyska/rss2twitter/app/rss"
)

type opts struct {
	Refresh      time.Duration `short:"r" long:"refresh" env:"REFRESH" default:"30s" description:"refresh interval"`
	TimeOut      time.Duration `short:"t" long:"timeout" env:"TIMEOUT" default:"5s" description:"rss feed timeout"`
	Feed         string        `short:"f" long:"feed" env:"FEED" required:"true" description:"rss feed url"`
	IncludeFirst bool          `long:"include-first" description:"start from the last current item, not with the next one"`

	Dry     bool `long:"dry" env:"DRY" description:"dry mode"`
	Dbg     bool `long:"dbg" env:"DEBUG" description:"debug mode"`
	Command struct {
		Name string
		Args []string
	} `positional-args:"yes"`
}

var revision = "unknown"

type notifier interface {
	Go(ctx context.Context) <-chan rss.Event
}

// CommandPublisher runs a command for every event
type CommandPublisher struct {
	Command []string
}

// Publish run the command for event. Most rss fields are translated as env vars
func (p CommandPublisher) Publish(event rss.Event, formatter func(rss.Event) string) error {
	log.Printf("[INFO] lancio - %s ", strings.Join(p.Command, "  "))
	cmd := exec.Command(p.Command[0], p.Command[1:]...)
	enclosuresUrls := []string{}
	for _, enclosure := range event.Item.Enclosures {
		enclosuresUrls = append(enclosuresUrls, url.QueryEscape(enclosure.URL))
	}
	cmd.Env = []string{"RSS=1",
		fmt.Sprintf("RSS_TITLE=%s", event.Title),
		fmt.Sprintf("RSS_GUID=%s", event.GUID),
		fmt.Sprintf("RSS_LINK=%s", event.Link),
		fmt.Sprintf("RSS_PUBDATE=%s", event.Item.Published),
		fmt.Sprintf("RSS_CATEGORIES=%s", strings.Join(event.Item.Categories, ";")),
		fmt.Sprintf("RSS_DESCRIPTION=%s", event.Item.Description),
		fmt.Sprintf("RSS_ENCLOSURES=%s", strings.Join(enclosuresUrls, ";")),
		// metadata that apply to the whole feed
		fmt.Sprintf("RSS_CHAN_TITLE=%s", event.Feed.Title),
		fmt.Sprintf("RSS_CHAN_LINK=%s", event.Feed.Link),
		fmt.Sprintf("RSS_CHAN_LANGUAGE=%s", event.Feed.Language),
	}
	if event.Item.Author != nil {
		cmd.Env = append(cmd.Env,
			fmt.Sprintf("RSS_AUTHOR=%s", event.Item.Author.Name),
			fmt.Sprintf("RSS_AUTHOR_EMAIL=%s", event.Item.Author.Email),
		)
	}
	if event.Feed.Author != nil {
		cmd.Env = append(cmd.Env,
			fmt.Sprintf("RSS_CHAN_AUTHOR=%s", event.Feed.Author.Name))
	}
	if event.Item.Image != nil {
		cmd.Env = append(cmd.Env, fmt.Sprintf("RSS_IMAGE=%s", event.Item.Image.URL))
	}
	textbuf := bytes.NewBuffer([]byte(event.Item.Content))
	cmd.Stdin = textbuf
	cmd.Stdout = os.Stderr
	cmdErr := cmd.Run()

	// TODO: move to goroutine?

	return cmdErr
}

func main() {
	fmt.Printf("rss2cmd - %s\n", revision)
	o := opts{}
	var err error
	if _, err = flags.Parse(&o); err != nil {
		os.Exit(2)
	}
	fmt.Println(o)
	fmt.Println(o.Command)
	if o.Command.Name != "" && o.Dry {
		fmt.Fprintln(os.Stderr, "Error: if you specify --dry you can't specify a command")
		os.Exit(2)
	}
	if o.Command.Name == "" && !o.Dry {
		fmt.Fprintln(os.Stderr, "Error: you must specify a command! (or use --dry)")
		os.Exit(2)
	}

	if o.Dbg {
		log.Setup(log.Debug)
	}

	notif, pub, err := setup(o)
	if err != nil {
		log.Printf("[PANIC] failed to setup, %v", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	go func() { // catch SIGTERM signal and invoke graceful termination
		stop := make(chan os.Signal, 1)
		signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
		<-stop
		log.Printf("[WARN] interrupt signal")
		cancel()
	}()

	do(ctx, notif, pub)
	log.Print("[INFO] terminated")
}

func setup(o opts) (n notifier, pub publisher.Interface, err error) {
	n = &rss.Notify{Feed: o.Feed, Duration: o.Refresh, Timeout: o.TimeOut, IncludeFirst: o.IncludeFirst}
	if o.Dry {
		pub = publisher.Stdout{}
	} else {
		cmd := append([]string{o.Command.Name}, o.Command.Args...)
		pub = CommandPublisher{Command: cmd}
		fmt.Println(cmd)
	}
	return n, pub, nil
}

// do runs event loop getting rss events and publishing them
func do(ctx context.Context, notif notifier, pub publisher.Interface) {
	ch := notif.Go(ctx)
	for event := range ch {
		err := pub.Publish(event, func(r rss.Event) string {
			return event.Title
		})
		if err != nil {
			log.Printf("[WARN] failed to publish, %s", err)
		}
	}
}

// format cleans text (removes html tags) and shrinks result
func format(inp string, max int) string {
	res := striphtmltags.StripTags(inp)
	if len([]rune(res)) > max {
		snippet := []rune(res)[:max]
		// go back in snippet and found first space
		for i := len(snippet) - 1; i >= 0; i-- {
			if snippet[i] == ' ' {
				snippet = snippet[:i]
				break
			}
		}
		res = string(snippet) + " ..."
	}
	return res
}

// getDump reads runtime stack and returns as a string
func getDump() string {
	maxSize := 5 * 1024 * 1024
	stacktrace := make([]byte, maxSize)
	length := runtime.Stack(stacktrace, true)
	if length > maxSize {
		length = maxSize
	}
	return string(stacktrace[:length])
}

func init() {
	// catch SIGQUIT and print stack traces
	sigChan := make(chan os.Signal)
	go func() {
		for range sigChan {
			log.Printf("[INFO] SIGQUIT detected, dump:\n%s", getDump())
		}
	}()
	signal.Notify(sigChan, syscall.SIGQUIT)
}
