package main

import (
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"git.polynom.me/rio/internal/acme"
	"git.polynom.me/rio/internal/certificates"
	"git.polynom.me/rio/internal/dns"
	"git.polynom.me/rio/internal/pages"
	"git.polynom.me/rio/internal/repo"
	"git.polynom.me/rio/internal/server"

	"code.gitea.io/sdk/gitea"
	"github.com/go-acme/lego/v4/challenge/http01"
	log "github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
)

func handleSubdomain(domain string, cname string, path, giteaUrl string, giteaClient *gitea.Client, w http.ResponseWriter) {
	hostParts := strings.Split(domain, ".")
	username := hostParts[0]

	// Strip the leading /
	if path[:1] == "/" {
		path = path[1:]
	}

	repo, path, err := repo.RepoFromPath(
		username,
		domain,
		cname,
		path,
		giteaClient,
	)
	if err != nil {
		log.Errorf("Failed to get repo: %s", err)
		w.WriteHeader(404)
		return
	}

	pages.ServeFile(username, repo.Name, path, giteaUrl, w)
}

func Handler(pagesDomain, giteaUrl string, giteaClient *gitea.Client) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		w.Header().Set("Server", "rio")

		if strings.HasSuffix(req.Host, pagesDomain) {
			log.Debug("Domain can be directly handled")
			handleSubdomain(req.Host, "", req.URL.Path, giteaUrl, giteaClient, w)
			return
		}

		cname, err := dns.LookupCNAME(req.Host)
		if err != nil {
			log.Warningf("CNAME request failed, we don't handle %s", req.Host)
			w.WriteHeader(400)
			return
		}
		log.Debugf("Got CNAME %s", cname)

		if strings.HasSuffix(cname, pagesDomain) {
			log.Debugf("%s is alias of %s and can be handled after a CNAME query", req.Host, cname)
			handleSubdomain(cname, cname, req.URL.Path, giteaUrl, giteaClient, w)
			return
		}

		log.Errorf("Not handling %s", req.Host)
		w.WriteHeader(404)
	}
}

func runServer(ctx *cli.Context) error {
	log.SetLevel(log.DebugLevel)
	giteaUrl := ctx.String("gitea-url")
	domain := ctx.String("pages-domain")
	certsFile := ctx.String("certs-file")
	acmeEmail := ctx.String("acme-email")
	acmeServer := ctx.String("acme-server")
	acmeFile := ctx.String("acme-file")
	acmeHost := ctx.String("acme-host")
	acmePort := ctx.String("acme-port")
	acmeDisable := ctx.Bool("acme-disable")

	// Setup the Gitea stuff
	httpClient := http.Client{Timeout: 10 * time.Second}
	client, err := gitea.NewClient(
		giteaUrl,
		gitea.SetHTTPClient(&httpClient),
		gitea.SetToken(""),
		gitea.SetUserAgent("rio"),
	)

	// Listen on the port
	addr := ctx.String("listen-host") + ":" + ctx.String("listen-port")
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		fmt.Errorf("Failed to create listener: %v", err)
		return err
	}

	if !acmeDisable {
		if acmeEmail == "" || acmeFile == "" || certsFile == "" {
			return errors.New("The options acme-file, acme-email, and certs-file are required")
		}

		cache, err := certificates.CertificateCacheFromFile(certsFile)
		if err != nil {
			log.Debugf("Generating cert")
			fallback, err := certificates.MakeFallbackCertificate(domain)
			if err != nil {
				log.Fatalf("Failed to generate fallback certificate: %v", err)
				return err
			}

			cache.FallbackCertificate = fallback
			cache.Certificates = make(map[string]certificates.CertificateWrapper)
			cache.FlushToDisk(certsFile)
			log.Debug("Certificate wrote to disk")
		} else {
			log.Debug("Certificate store read from disk")
		}

		// Create an ACME client, if we failed to load one
		acmeClient, err := acme.ClientFromFile(acmeFile, acmeServer)
		if err != nil {
			log.Warn("Failed to load ACME client data from disk. Generating new account")
			acmeClient, err = acme.GenerateNewAccount(acmeEmail, acmeFile, acmeServer)
			if err != nil {
				log.Fatalf("Failed to generate new ACME client: %v", err)
				return err
			}
			log.Info("ACME account registered")
		} else {
			log.Info("ACME client data read from disk")
		}

		// Set up the HTTP01 listener
		err = acmeClient.Challenge.SetHTTP01Provider(
			http01.NewProviderServer(acmeHost, acmePort),
		)
		if err != nil {
			log.Fatalf("Failed to setup HTTP01 challenge listener: %v", err)
			return err
		}

		tlsConfig := server.MakeTlsConfig(
			domain,
			certsFile,
			&cache,
			acmeClient,
		)
		listener = tls.NewListener(listener, tlsConfig)
	}

	if err := http.Serve(listener, Handler(domain, giteaUrl, client)); err != nil {
		fmt.Printf("Listening failed")
		return err
	}

	return nil
}

func main() {
	app := &cli.App{
		Action: runServer,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:     "gitea-url",
				Usage:    "The (HTTPS) URL to the serving Gitea instance",
				EnvVars:  []string{"GITEA_URL"},
				Required: true,
			},
			&cli.StringFlag{
				Name:    "listen-host",
				Usage:   "The host to listen on",
				EnvVars: []string{"HOST"},
				Value:   "127.0.0.1",
			},
			&cli.StringFlag{
				Name:    "listen-port",
				Usage:   "The port to listen on",
				EnvVars: []string{"PORT"},
				Value:   "8888",
			},
			&cli.StringFlag{
				Name:    "acme-host",
				Usage:   "The host to bind to for ACME challenges",
				EnvVars: []string{"ACME_HOST"},
				Value:   "",
			},
			&cli.StringFlag{
				Name:    "acme-port",
				Usage:   "The port to listen on for ACME challenges",
				EnvVars: []string{"ACME_PORT"},
				Value:   "8889",
			},
			&cli.StringFlag{
				Name:     "pages-domain",
				Usage:    "The domain on which the server is reachable",
				EnvVars:  []string{"PAGES_DOMAIN"},
				Required: true,
			},
			&cli.StringFlag{
				Name:    "certs-file",
				Usage:   "File to store certificates in",
				EnvVars: []string{"CERTS_FILE"},
				Value:   "",
			},
			&cli.StringFlag{
				Name:    "acme-file",
				Usage:   "File to store ACME configuration in",
				EnvVars: []string{"ACME_FILE"},
				Value:   "",
			},
			&cli.StringFlag{
				Name:    "acme-email",
				Usage:   "Email to use for an ACME account",
				EnvVars: []string{"ACME_EMAIL"},
				Value:   "",
			},
			&cli.StringFlag{
				Name:    "acme-server",
				Usage:   "CA Directory to use",
				EnvVars: []string{"ACME_SERVER"},
				Value:   "https://acme-staging-v02.api.letsencrypt.org/directory",
			},
			&cli.BoolFlag{
				Name:    "acme-disable",
				Usage:   "Whether to disable automatic ACME certificates",
				EnvVars: []string{"ACME_DISABLE"},
			},
		},
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatalf("Failed to run app: %s", err)
	}
}
