package vain

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/mail"
	"strings"
	"time"

	"github.com/elazarl/go-bindata-assetfs"

	verrors "mcquay.me/vain/errors"
)

const apiPrefix = "/api/v0/"

var prefix map[string]string

func init() {
	prefix = map[string]string{
		"pkgs":     apiPrefix + "db/",
		"register": apiPrefix + "register/",
		"confirm":  apiPrefix + "confirm/",
		"forgot":   apiPrefix + "forgot/",
		"static":   "/_static/",
	}
}

// NewServer populates a server, adds the routes, and returns it for use.
func NewServer(sm *http.ServeMux, store *DB, static string, emailTimeout time.Duration) *Server {
	s := &Server{
		db:           store,
		static:       static,
		emailTimeout: emailTimeout,
	}
	addRoutes(sm, s)
	return s
}

// Server serves up the http.
type Server struct {
	db           *DB
	static       string
	emailTimeout time.Duration
}

func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.Method == "GET" {
		req.ParseForm()
		if _, ok := req.Form["go-get"]; !ok {
			http.Redirect(w, req, prefix["static"], http.StatusTemporaryRedirect)
			return
		}
		fmt.Fprintf(w, "<!DOCTYPE html>\n<html><head>\n")
		for _, p := range s.db.Pkgs() {
			fmt.Fprintf(w, "%s\n", p)
		}
		fmt.Fprintf(w, "</head>\n<body><p>go tool metadata in head</p></body>\n</html>\n")
		return
	}

	const prefix = "Bearer "
	var tok string
	auth := req.Header.Get("Authorization")
	if strings.HasPrefix(auth, prefix) {
		tok = strings.TrimPrefix(auth, prefix)
	}
	if tok == "" {
		http.Error(w, "missing token", http.StatusUnauthorized)
		return
	}
	ns, err := parseNamespace(req.URL.Path)
	if err != nil {
		http.Error(w, fmt.Sprintf("could not parse namespace:%v", err), http.StatusBadRequest)
		return
	}

	if err := verrors.ToHTTP(s.db.NSForToken(ns, tok)); err != nil {
		http.Error(w, err.Message, err.Code)
		return
	}

	switch req.Method {
	case "POST":
		if req.URL.Path == "/" {
			http.Error(w, fmt.Sprintf("invalid path %q", req.URL.Path), http.StatusBadRequest)
			return
		}
		p := Package{}
		if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
			http.Error(w, fmt.Sprintf("unable to parse json from body: %v", err), http.StatusBadRequest)
			return
		}
		if p.Repo == "" {
			http.Error(w, fmt.Sprintf("invalid repository %q", p.Repo), http.StatusBadRequest)
			return
		}
		if p.Vcs == "" {
			p.Vcs = "git"
		}
		if !valid(p.Vcs) {
			http.Error(w, fmt.Sprintf("invalid vcs %q", p.Vcs), http.StatusBadRequest)
			return
		}
		p.Path = fmt.Sprintf("%s/%s", req.Host, strings.Trim(req.URL.Path, "/"))
		p.Ns = ns
		if !Valid(p.Path, s.db.Pkgs()) {
			http.Error(w, fmt.Sprintf("invalid path; prefix already taken %q", req.URL.Path), http.StatusConflict)
			return
		}
		if err := s.db.AddPackage(p); err != nil {
			http.Error(w, fmt.Sprintf("unable to add package: %v", err), http.StatusInternalServerError)
			return
		}
	case "DELETE":
		p := fmt.Sprintf("%s/%s", req.Host, strings.Trim(req.URL.Path, "/"))
		if !s.db.PackageExists(p) {
			http.Error(w, fmt.Sprintf("package %q not found", p), http.StatusNotFound)
			return
		}

		if err := s.db.RemovePackage(p); err != nil {
			http.Error(w, fmt.Sprintf("unable to delete package: %v", err), http.StatusInternalServerError)
			return
		}
	default:
		http.Error(w, fmt.Sprintf("unsupported method %q; accepted: POST, GET, DELETE", req.Method), http.StatusMethodNotAllowed)
	}
}

func (s *Server) register(w http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	email, ok := req.Form["email"]
	if !ok || len(email) != 1 {
		http.Error(w, "must provide one email parameter", http.StatusBadRequest)
		return
	}

	addr := email[0]
	if _, err := mail.ParseAddress(addr); err != nil {
		http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest)
		return
	}

	tok, err := s.db.Register(addr)
	if err := verrors.ToHTTP(err); err != nil {
		http.Error(w, err.Message, err.Code)
		return
	}
	proto := "https"
	if req.TLS == nil {
		proto = "http"
	}
	log.Printf("%s://%s/api/v0/confirm/%+v", proto, req.Host, tok)
	resp := struct {
		Msg string `json:"msg"`
	}{
		Msg: "please check your email\n",
	}
	w.Header().Set("Content-type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func (s *Server) confirm(w http.ResponseWriter, req *http.Request) {
	tok := req.URL.Path[len(prefix["confirm"]):]
	tok = strings.TrimRight(tok, "/")
	if tok == "" {
		http.Error(w, "must provide one email parameter", http.StatusBadRequest)
		return
	}
	tok, err := s.db.Confirm(tok)
	if err := verrors.ToHTTP(err); err != nil {
		http.Error(w, err.Message, err.Code)
		return
	}
	fmt.Fprintf(w, "new token: %s\n", tok)
}

func (s *Server) forgot(w http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	email, ok := req.Form["email"]
	if !ok || len(email) != 1 {
		http.Error(w, "must provide one email parameter", http.StatusBadRequest)
		return
	}

	addr := email[0]
	if _, err := mail.ParseAddress(addr); err != nil {
		http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest)
		return
	}

	tok, err := s.db.forgot(addr, s.emailTimeout)
	if err := verrors.ToHTTP(err); err != nil {
		http.Error(w, err.Message, err.Code)
		return
	}
	log.Printf("http://%s/api/v0/confirm/%+v", req.Host, tok)
	resp := struct {
		Msg string `json:"msg"`
	}{
		Msg: "please check your email\n",
	}
	w.Header().Set("Content-type", "application/json")
	json.NewEncoder(w).Encode(resp)
}
func (s *Server) pkgs(w http.ResponseWriter, req *http.Request) {
	w.Header().Set("Content-type", "application/json")
	json.NewEncoder(w).Encode(s.db.Pkgs())
}

func addRoutes(sm *http.ServeMux, s *Server) {
	sm.Handle("/", s)

	if s.static == "" {
		sm.Handle(
			prefix["static"],
			http.FileServer(
				&assetfs.AssetFS{
					Asset:     Asset,
					AssetDir:  AssetDir,
					AssetInfo: AssetInfo,
				},
			),
		)
	} else {
		sm.Handle(
			prefix["static"],
			http.StripPrefix(
				prefix["static"],
				http.FileServer(http.Dir(s.static)),
			),
		)
	}

	sm.HandleFunc(prefix["pkgs"], s.pkgs)
	sm.HandleFunc(prefix["register"], s.register)
	sm.HandleFunc(prefix["confirm"], s.confirm)
	sm.HandleFunc(prefix["forgot"], s.forgot)
}
