package api

import (
	"crypto/rand"
	"crypto/sha1"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/base32"
	"encoding/base64"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"math/big"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"srs.epita.fr/fic-server/admin/pki"
	"srs.epita.fr/fic-server/libfic"

	"github.com/gin-gonic/gin"
)

var TeamsDir string

func declareCertificateRoutes(router *gin.RouterGroup) {
	router.GET("/htpasswd", func(c *gin.Context) {
		ret, err := genHtpasswd(true)
		if err != nil {
			c.AbortWithError(http.StatusInternalServerError, err)
			return
		}
		c.String(http.StatusOK, ret)
	})
	router.POST("/htpasswd", func(c *gin.Context) {
		if htpasswd, err := genHtpasswd(true); err != nil {
			log.Println("Unable to generate htpasswd:", err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil {
			log.Println("Unable to write htpasswd:", err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}
		c.AbortWithStatus(http.StatusOK)
	})
	router.DELETE("/htpasswd", func(c *gin.Context) {
		if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
			log.Println("Unable to remove htpasswd:", err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}
		c.AbortWithStatus(http.StatusOK)
	})
	router.GET("/htpasswd.apr1", func(c *gin.Context) {
		ret, err := genHtpasswd(false)
		if err != nil {
			c.AbortWithError(http.StatusInternalServerError, err)
			return
		}
		c.String(http.StatusOK, ret)
	})
	router.GET("/ca", infoCA)
	router.GET("/ca.pem", getCAPEM)
	router.POST("/ca/new", func(c *gin.Context) {
		var upki PKISettings
		err := c.ShouldBindJSON(&upki)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
			return
		}

		if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusCreated, true)
	})

	router.GET("/certs", getCertificates)
	router.POST("/certs", generateClientCert)
	router.DELETE("/certs", func(c *gin.Context) {
		v, err := fic.ClearCertificates()
		if err != nil {
			log.Println("Unable to ClearCertificates:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, v)
	})

	apiCertificatesRoutes := router.Group("/certs/:certid")
	apiCertificatesRoutes.Use(CertificateHandler)
	apiCertificatesRoutes.HEAD("", getTeamP12File)
	apiCertificatesRoutes.GET("", getTeamP12File)
	apiCertificatesRoutes.PUT("", updateCertificateAssociation)
	apiCertificatesRoutes.DELETE("", func(c *gin.Context) {
		cert := c.MustGet("cert").(*fic.Certificate)

		v, err := cert.Revoke()
		if err != nil {
			log.Println("Unable to Revoke:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, v)
	})
}

func declareTeamCertificateRoutes(router *gin.RouterGroup) {
	router.GET("/certificates", func(c *gin.Context) {
		team := c.MustGet("team").(*fic.Team)

		if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
			log.Println("Unable to GetTeamSerials:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		} else {
			var certs []CertExported
			for _, serial := range serials {
				if cert, err := fic.GetCertificate(serial); err == nil {
					certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked})
				} else {
					log.Println("Unable to get back certificate, whereas an association exists on disk: ", err)
				}
			}
			c.JSON(http.StatusOK, certs)
		}
	})

	router.GET("/associations", func(c *gin.Context) {
		team := c.MustGet("team").(*fic.Team)

		assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
		if err != nil {
			log.Println("Unable to GetTeamAssociations:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, assocs)
	})

	apiTeamAssociationsRoutes := router.Group("/associations/:assoc")
	apiTeamAssociationsRoutes.POST("", func(c *gin.Context) {
		team := c.MustGet("team").(*fic.Team)

		if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil {
			log.Println("Unable to create association symlink:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())})
			return
		}

		c.JSON(http.StatusOK, c.Params.ByName("assoc"))
	})
	apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) {
		err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc"))
		if err != nil {
			log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())})
			return
		}

		c.JSON(http.StatusOK, nil)
	})

}

func CertificateHandler(c *gin.Context) {
	var certid uint64
	var err error

	cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12")
	if certid, err = strconv.ParseUint(cid, 10, 64); err != nil {
		if certid, err = strconv.ParseUint(cid, 16, 64); err != nil {
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"})
			return
		}
	}

	cert, err := fic.GetCertificate(certid)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"})
		return
	}

	c.Set("cert", cert)

	c.Next()
}

func genHtpasswd(ssha bool) (ret string, err error) {
	var teams []*fic.Team
	teams, err = fic.GetTeams()
	if err != nil {
		return
	}

	for _, team := range teams {
		var serials []uint64
		serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
		if err != nil {
			return
		}

		if len(serials) == 0 {
			// Don't include teams that don't have associated certificates
			continue
		}

		for _, serial := range serials {
			var cert *fic.Certificate
			cert, err = fic.GetCertificate(serial)
			if err != nil {
				// Ignore invalid/incorrect/non-existant certificates
				continue
			}

			if cert.Revoked != nil {
				continue
			}

			salt := make([]byte, 5)
			if _, err = rand.Read(salt); err != nil {
				return
			}

			if ssha {
				hash := sha1.New()
				hash.Write([]byte(cert.Password))
				hash.Write([]byte(salt))

				passwdline := fmt.Sprintf(":{SSHA}%s\n", base64.StdEncoding.EncodeToString(append(hash.Sum(nil), salt...)))

				ret += strings.ToLower(team.Name) + passwdline
				ret += fmt.Sprintf("%0[2]*[1]x", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
				ret += fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
				teamAssociations, _ := pki.GetTeamAssociations(TeamsDir, team.Id)
				log.Println(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)), teamAssociations)
				for _, ta := range teamAssociations {
					ret += strings.Replace(ta, ":", "", -1) + passwdline
				}
			} else {
				salt32 := base32.StdEncoding.EncodeToString(salt)
				ret += fmt.Sprintf(
					"%s:$apr1$%s$%s\n",
					strings.ToLower(team.Name),
					salt32,
					fic.Apr1Md5(cert.Password, salt32),
				)
			}
		}
	}

	return
}

type PKISettings struct {
	Version            int                     `json:"version"`
	SerialNumber       *big.Int                `json:"serialnumber"`
	Issuer             pkix.Name               `json:"issuer"`
	Subject            pkix.Name               `json:"subject"`
	NotBefore          time.Time               `json:"notbefore"`
	NotAfter           time.Time               `json:"notafter"`
	SignatureAlgorithm x509.SignatureAlgorithm `json:"signatureAlgorithm,"`
	PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"`
}

func infoCA(c *gin.Context) {
	_, cacert, err := pki.LoadCA()
	if err != nil {
		c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"})
		return
	}

	c.JSON(http.StatusOK, PKISettings{
		Version:            cacert.Version,
		SerialNumber:       cacert.SerialNumber,
		Issuer:             cacert.Issuer,
		Subject:            cacert.Subject,
		NotBefore:          cacert.NotBefore,
		NotAfter:           cacert.NotAfter,
		SignatureAlgorithm: cacert.SignatureAlgorithm,
		PublicKeyAlgorithm: cacert.PublicKeyAlgorithm,
	})
}

func getCAPEM(c *gin.Context) {
	if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
		c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"})
		return
	} else if fd, err := os.Open(pki.CACertPath()); err != nil {
		log.Println("Unable to open CA root certificate:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	} else {
		defer fd.Close()

		cnt, err := ioutil.ReadAll(fd)
		if err != nil {
			log.Println("Unable to read CA root certificate:", err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.String(http.StatusOK, string(cnt))
	}
}

func getTeamP12File(c *gin.Context) {
	cert := c.MustGet("cert").(*fic.Certificate)

	// Create p12 if necessary
	if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
		if err := pki.WriteP12(cert.Id, cert.Password); err != nil {
			log.Println("Unable to WriteP12:", err.Error())
			c.AbortWithError(http.StatusInternalServerError, err)
			return
		}
	}

	if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
		log.Println("Unable to compute ClientP12Path:", err.Error())
		c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?"))
		return
	} else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
		log.Println("Unable to open ClientP12Path:", err.Error())
		c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
		return
	} else {
		defer fd.Close()

		data, err := ioutil.ReadAll(fd)
		if err != nil {
			log.Println("Unable to open ClientP12Path:", err.Error())
			c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
			return
		}

		c.Data(http.StatusOK, "application/x-pkcs12", data)
	}
}

func generateClientCert(c *gin.Context) {
	// First, generate a new, unique, serial
	var serial_gen [8]byte
	if _, err := rand.Read(serial_gen[:]); err != nil {
		log.Println("Unable to read enough entropy to generate client certificate:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
		return
	}
	for fic.ExistingCertSerial(serial_gen) {
		if _, err := rand.Read(serial_gen[:]); err != nil {
			log.Println("Unable to read enough entropy to generate client certificate:", err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
			return
		}
	}

	var serial_b big.Int
	serial_b.SetBytes(serial_gen[:])
	serial := serial_b.Uint64()

	// Let's pick a random password
	password, err := fic.GeneratePassword()
	if err != nil {
		log.Println("Unable to generate password:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()})
		return
	}

	// Ok, now load CA
	capriv, cacert, err := pki.LoadCA()
	if err != nil {
		log.Println("Unable to load the CA:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"})
		return
	}

	// Generate our privkey
	if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
		log.Println("Unable to generate private key:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()})
		return
	}

	// Save in DB
	cert, err := fic.RegisterCertificate(serial, password)
	if err != nil {
		log.Println("Unable to register certificate:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."})
		return
	}

	c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked})
}

type CertExported struct {
	Id       string     `json:"id"`
	Creation time.Time  `json:"creation"`
	Password string     `json:"password,omitempty"`
	IdTeam   *int64     `json:"id_team"`
	Revoked  *time.Time `json:"revoked"`
}

func getCertificates(c *gin.Context) {
	certificates, err := fic.GetCertificates()
	if err != nil {
		log.Println("Unable to retrieve certificates list:", err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."})
		return
	}
	ret := make([]CertExported, 0)
	for _, cert := range certificates {
		dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))

		var idTeam *int64 = nil
		if lnk, err := os.Readlink(dstLinkPath); err == nil {
			if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil {
				idTeam = &tid
			}
		}

		ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked})
	}

	c.JSON(http.StatusOK, ret)
}

type CertUploaded struct {
	Team *int64 `json:"id_team"`
}

func updateCertificateAssociation(c *gin.Context) {
	cert := c.MustGet("cert").(*fic.Certificate)

	var uc CertUploaded
	err := c.ShouldBindJSON(&uc)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
		return
	}

	dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))

	if uc.Team != nil {
		srcLinkPath := fmt.Sprintf("%d", *uc.Team)
		if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
			log.Println("Unable to create certificate symlink:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())})
			return
		}

		// Mark team as active to ensure it'll be generated
		if ut, err := fic.GetTeam(*uc.Team); err != nil {
			log.Println("Unable to GetTeam:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."})
			return
		} else if !ut.Active {
			ut.Active = true
			_, err := ut.Update()
			if err != nil {
				log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error())
			}
		}
	} else {
		os.Remove(dstLinkPath)
	}

	c.JSON(http.StatusOK, cert)
}
