package fe

import (
	"bytes"
	"fmt"
	"html/template"
	"io"
	"log"
	"math/rand"
	"net"
	"net/http"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	_ "net/http/pprof"

	"git.autistici.org/ale/radioai"
	"github.com/PuerkitoBio/ghost/handlers"
)

// HTTP redirector.
type HttpRedirector struct {
	domain   string
	client   *radioai.RadioAPI
	template *template.Template
}

func NewHttpRedirector(client *radioai.RadioAPI, domain string) *HttpRedirector {
	return &HttpRedirector{
		client: client,
		domain: domain,
	}
}

// Return an active node, chosen randomly (this is currently our load
// balancing policy, since there is no status information about the
// nodes yet).
func (h *HttpRedirector) pickActiveNode() string {
	nodes, _ := h.client.GetNodes()
	if nodes != nil && len(nodes) > 0 {
		return nodes[rand.Intn(len(nodes))]
	}
	return ""
}

// Parse the request and extract the mount path.
func (h *HttpRedirector) getMount(r *http.Request) (*radioai.Mount, error) {
	path := r.URL.Path
	if strings.HasSuffix(path, ".m3u") {
		path = path[:len(path)-4]
	}
	return h.client.GetMount(path)
}

func makeIcecastUrl(server string) string {
	return net.JoinHostPort(server, strconv.Itoa(radioai.IcecastPort))
}

// Serve a response for a client connection to a relay.
func (h *HttpRedirector) serveRelay(w http.ResponseWriter, r *http.Request) {
	mount, err := h.getMount(r)
	if err != nil {
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	// Find an active node.
	relayAddr := h.pickActiveNode()
	if relayAddr == "" {
		http.Error(w, "No active nodes", http.StatusServiceUnavailable)
		return
	}

	// Create the m3u response.
	m3u := fmt.Sprintf("http://%s%s\n", makeIcecastUrl(relayAddr), mount.Name)
	w.Header().Set("Content-Length", strconv.Itoa(len(m3u)))
	w.Header().Set("Content-Type", "audio/x-mpegurl")
	w.Header().Set("Expires", "-1")
	w.Header().Set("Cache-Control", "private, max-age=0")
	io.WriteString(w, m3u)
}

func (h *HttpRedirector) serveSource(w http.ResponseWriter, r *http.Request) {
	_, err := h.getMount(r)
	if err != nil {
		log.Printf("source: error retrieving mount for %+v: %s", r, err)
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	// Find the current master node.
	masterAddr, err := h.client.GetMasterAddr()
	if err != nil {
		log.Printf("source: no master: %s", err)
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
		return
	}

	// Hijack the incoming connection. This is necessary, rather
	// than using httputil.ReverseProxy, for two important
	// reasons:
	//
	// 1) So that we can reset the timeout on the underlying
	// network connection (which we have no use for once the
	// stream has been established).
	//
	// 2) Because streaming is still mostly a HTTP/1.0 world, the
	// HTTP/1.1 features used by Go's net/http package (mostly the
	// chunked encoding, I think) will apparently confuse clients
	// and servers alike.
	//
	conn, _, err := w.(http.Hijacker).Hijack()
	if err != nil {
		log.Printf("source: hijack failed: %v", err)
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
		return
	}
	defer conn.Close()
	if err := conn.SetDeadline(time.Time{}); err != nil {
		log.Printf("source: could not reset deadline: %v", err)
	}

	// Create the upstream connection, and write the original
	// request to it as-is (the URL path on the backend is the
	// same, and the headers do not need to change).
	upstream, err := net.Dial("tcp", makeIcecastUrl(masterAddr))
	if err != nil {
		log.Printf("source: dial upstream: %v", err)
		return
	}
	defer upstream.Close()
	if err := r.Write(upstream); err != nil {
		log.Printf("source: write upstream request: %v", err)
		return
	}

	// Start two copiers, one for the source data, one for the
	// replies. Wait until both are done.
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		if _, err := io.Copy(conn, upstream); err != nil {
			log.Printf("upstream -> source: Copy: %v", err)
		}
		wg.Done()
	}()
	go func() {
		if _, err := io.Copy(upstream, conn); err != nil {
			log.Printf("source -> upstream: Copy: %v", err)
		}
		wg.Done()
	}()
	wg.Wait()
}

func (h *HttpRedirector) serveStatusPage(w http.ResponseWriter, r *http.Request) {
	nodes, _ := h.client.GetNodes()
	mounts, _ := h.client.ListMounts()
	ctx := struct {
		Domain string
		Nodes  []string
		Mounts []*radioai.Mount
	}{h.domain, nodes, mounts}

	var buf bytes.Buffer
	if err := h.template.ExecuteTemplate(&buf, "index.html", ctx); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
	w.Write(buf.Bytes())
}

// Run starts the HTTP server on the given addr. Does not return.
func (h *HttpRedirector) Run(addr, staticDir, templateDir string) {
	h.template = template.Must(
		template.ParseGlob(
			filepath.Join(templateDir, "*.html")))

	// Create our HTTP handler stack. Passes the /debug/ queries
	// along to the global ServeMux (where moodules such as pprof
	// install their handlers).
	mux := http.NewServeMux()
	mux.Handle(
		"/static/",
		http.StripPrefix(
			"/static/",
			http.FileServer(http.Dir(staticDir))))
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "" || r.URL.Path == "/":
			h.serveStatusPage(w, r)
		case strings.HasPrefix(r.URL.Path, "/debug/"):
			http.DefaultServeMux.ServeHTTP(w, r)
		default:
			h.serveRelay(w, r)
		}
	})

	// Add some handlers to support gzip-encoded responses and
	// request logging.
	wraph := handlers.GZIPHandler(mux, nil)
	logopts := handlers.NewLogOptions(nil, handlers.Lshort)
	wraph = handlers.LogHandler(wraph, logopts)

	// Serve SOURCE requests bypassing the logging and gzip
	// handlers: since they wrap the ResponseWriter, we would be
	// unable to hijack the underlying connection for proxying.
	rooth := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "SOURCE" {
			h.serveSource(w, r)
		} else {
			wraph.ServeHTTP(w, r)
		}
	})

	httpServer := &http.Server{
		Addr:         addr,
		Handler:      rooth,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	log.Printf("starting HTTP server on %s/tcp", httpServer.Addr)
	log.Fatal(httpServer.ListenAndServe())
}