// Copyright (C) 2019 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/alecthomas/kong"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	_ "github.com/syncthing/syncthing/lib/automaxprocs"
	"github.com/syncthing/syncthing/lib/httpcache"
	"github.com/syncthing/syncthing/lib/upgrade"
)

type cli struct {
	Listen        string        `default:":8080" help:"Listen address"`
	MetricsListen string        `default:":8081" help:"Listen address for metrics"`
	URL           string        `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
	Forward       []string      `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
	CacheTime     time.Duration `default:"15m" help:"Cache time"`
}

func main() {
	var params cli
	kong.Parse(&params)
	if err := server(&params); err != nil {
		fmt.Printf("Error: %v\n", err)
		os.Exit(1)
	}
}

func server(params *cli) error {
	if params.MetricsListen != "" {
		mux := http.NewServeMux()
		mux.Handle("/metrics", promhttp.Handler())
		go func() {
			log.Println("Listening for metrics on", params.MetricsListen)
			if err := http.ListenAndServe(params.MetricsListen, mux); err != nil {
				log.Fatalf("Failed to start metrics server: %v", err)
			}
		}()
	}

	mux := http.NewServeMux()
	mux.Handle("/meta.json", httpcache.SinglePath(&githubReleases{url: params.URL}, params.CacheTime))

	for _, fwd := range params.Forward {
		path, url, ok := strings.Cut(fwd, "->")
		if !ok {
			return fmt.Errorf("invalid forward: %q", fwd)
		}
		log.Println("Forwarding", path, "to", url)
		mux.Handle(path, httpcache.SinglePath(&proxy{url: url}, params.CacheTime))
	}

	srv := &http.Server{
		Addr:         params.Listen,
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	srv.SetKeepAlivesEnabled(false)
	return srv.ListenAndServe()
}

type githubReleases struct {
	url string
}

func (p *githubReleases) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
	log.Println("Fetching", p.url)
	rels := upgrade.FetchLatestReleases(p.url, "")
	if rels == nil {
		http.Error(w, "no releases", http.StatusInternalServerError)
		return
	}

	sort.Sort(upgrade.SortByRelease(rels))
	rels = filterForLatest(rels)

	// Move the URL used for browser downloads to the URL field, and remove
	// the browser URL field. This avoids going via the GitHub API for
	// downloads, since Syncthing uses the URL field.
	for _, rel := range rels {
		for j, asset := range rel.Assets {
			rel.Assets[j].URL = asset.BrowserURL
			rel.Assets[j].BrowserURL = ""
		}
	}

	buf := new(bytes.Buffer)
	_ = json.NewEncoder(buf).Encode(rels)

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "GET")
	w.Write(buf.Bytes())
}

type proxy struct {
	url string
}

func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	log.Println("Fetching", p.url)
	req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	ct := resp.Header.Get("Content-Type")
	w.Header().Set("Content-Type", ct)
	if resp.StatusCode == http.StatusOK {
		w.Header().Set("Cache-Control", "public, max-age=900")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET")
	}
	w.WriteHeader(resp.StatusCode)
	if strings.HasPrefix(ct, "application/json") {
		// Special JSON handling; clean it up a bit.
		var v interface{}
		if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(v)
	} else {
		_, _ = io.Copy(w, resp.Body)
	}
}

// filterForLatest returns the latest stable and prerelease only. If the
// stable version is newer (comes first in the list) there is no need to go
// looking for a prerelease at all.
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
	var filtered []upgrade.Release
	var havePre bool
	for _, rel := range rels {
		if !rel.Prerelease {
			// We found a stable version, we're good now.
			filtered = append(filtered, rel)
			break
		}
		if rel.Prerelease && !havePre {
			// We remember the first prerelease we find.
			filtered = append(filtered, rel)
			havePre = true
		}
	}
	return filtered
}
