package api

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"strings"
	"text/template"
	"unicode"

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

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

var (
	OidcIssuer   = "live.fic.srs.epita.fr"
	OidcClientId = "epita-challenge"
	OidcSecret   = ""
)

func declarePasswordRoutes(router *gin.RouterGroup) {
	router.POST("/password", func(c *gin.Context) {
		passwd, err := fic.GeneratePassword()
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"password": passwd})
	})
	router.GET("/dex.yaml", func(c *gin.Context) {
		cfg, err := genDexConfig()
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.String(http.StatusOK, string(cfg))
	})
	router.POST("/dex.yaml", func(c *gin.Context) {
		if dexcfg, err := genDexConfig(); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, true)
	})
	router.GET("/dex-password.tpl", func(c *gin.Context) {
		passtpl, err := genDexPasswordTpl()
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.String(http.StatusOK, string(passtpl))
	})
	router.POST("/dex-password.tpl", func(c *gin.Context) {
		if dexcfg, err := genDexPasswordTpl(); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, true)
	})
	router.GET("/vouch-proxy.yaml", func(c *gin.Context) {
		cfg, err := genVouchProxyConfig()
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.String(http.StatusOK, string(cfg))
	})
	router.POST("/vouch-proxy.yaml", func(c *gin.Context) {
		if dexcfg, err := genVouchProxyConfig(); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "vouch-config.yaml"), []byte(dexcfg), 0644); err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

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

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

		if team.Password != nil {
			c.String(http.StatusOK, *team.Password)
		} else {
			c.AbortWithStatusJSON(http.StatusNotFound, nil)
		}
	})
	router.POST("/password", func(c *gin.Context) {
		team := c.MustGet("team").(*fic.Team)

		if passwd, err := fic.GeneratePassword(); err != nil {
			log.Println("Unable to GeneratePassword:", err.Error())
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when generating the new team password"})
			return
		} else {
			team.Password = &passwd

			_, err := team.Update()
			if err != nil {
				log.Println("Unable to Update Team:", err.Error())
				c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when updating the new team password"})
				return
			}

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

const dexcfgtpl = `issuer: {{ .Issuer }}
storage:
  type: sqlite3
  config:
    file: /var/dex/dex.db
web:
  http: 0.0.0.0:5556
frontend:
  issuer: Challenge forensic
  logoURL: {{ .LogoPath }}
  dir: /srv/dex/web/
oauth2:
  skipApprovalScreen: true
staticClients:
{{ range $c := .Clients }}
- id: {{ $c.Id }}
  name: {{ $c.Name }}
  redirectURIs: [{{ range $u := $c.RedirectURIs }}'{{ $u }}'{{ end }}]
  secret: {{ $c.Secret }}
{{ end }}
enablePasswordDB: true
staticPasswords:
{{ range $t := .Teams }}
  - email: "team{{ printf "%02d" $t.Id }}"
    hash: "{{with $t }}{{ .HashedPassword }}{{end}}"
{{ end }}
`

const dexpasswdtpl = `{{ "{{" }} template "header.html" . {{ "}}" }}

<div class="theme-panel">
  <h2 class="theme-heading">
    Bienvenue au {{ .Name }}&nbsp;!
  </h2>
  <form method="post" action="{{ "{{" }} .PostURL {{ "}}" }}">
    <div class="theme-form-row">
      <div class="theme-form-label">
        <label for="userid">Votre équipe</label>
      </div>
      <select tabindex="1" required id="login" name="login" class="theme-form-input" autofocus>
{{ range $t := .Teams }}        <option value="team{{ printf "%02d" $t.Id }}">{{ $t.Name }}</option>
{{ end }}      </select>
    </div>
    <div class="theme-form-row">
      <div class="theme-form-label">
        <label for="password">Mot de passe</label>
      </div>
	  <input tabindex="2" required id="password" name="password" type="password" class="theme-form-input" placeholder="mot de passe" {{ "{{" }} if .Invalid {{ "}}" }} autofocus {{ "{{" }} end {{ "}}" }}/>
    </div>

    {{ "{{" }} if .Invalid {{ "}}" }}
      <div id="login-error" class="dex-error-box">
        Identifiants incorrects.
      </div>
    {{ "{{" }} end {{ "}}" }}

    <button tabindex="3" id="submit-login" type="submit" class="dex-btn theme-btn--primary">C'est parti&nbsp;!</button>

  </form>
  {{ "{{" }} if .BackLink {{ "}}" }}
  <div class="theme-link-back">
    <a class="dex-subtle-text" href="{{ "{{" }} .BackLink {{ "}}" }}">Sélectionner une autre méthode d'authentification.</a>
  </div>
  {{ "{{" }} end {{ "}}" }}
</div>

{{ "{{" }} template "footer.html" . {{ "}}" }}
`

type dexConfigClient struct {
	Id           string
	Name         string
	RedirectURIs []string
	Secret       string
}

type dexConfig struct {
	Name     string
	Issuer   string
	Clients  []dexConfigClient
	Teams    []*fic.Team
	LogoPath string
}

func genDexConfig() ([]byte, error) {
	if OidcSecret == "" {
		return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
	}

	teams, err := fic.GetTeams()
	if err != nil {
		return nil, err
	}

	b := bytes.NewBufferString("")

	challengeInfo, err := GetChallengeInfo()
	if err != nil {
		return nil, fmt.Errorf("Cannot create template: %w", err)
	}

	// Lower the first letter to be included in a sentence.
	name := []rune(challengeInfo.Title)
	if len(name) > 0 {
		name[0] = unicode.ToLower(name[0])
	}

	logoPath := ""
	if len(challengeInfo.MainLogo) > 0 {
		logoPath = strings.Replace(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1], "$FILES$", fic.FilesDir, -1)
	}

	dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
	if err != nil {
		return nil, fmt.Errorf("Cannot create template: %w", err)
	}

	err = dexTmpl.Execute(b, dexConfig{
		Name:   string(name),
		Issuer: "https://" + OidcIssuer,
		Clients: []dexConfigClient{
			dexConfigClient{
				Id:           OidcClientId,
				Name:         challengeInfo.Title,
				RedirectURIs: []string{"https://" + OidcIssuer + "/challenge_access/auth"},
				Secret:       OidcSecret,
			},
		},
		Teams:    teams,
		LogoPath: logoPath,
	})
	if err != nil {
		return nil, fmt.Errorf("An error occurs during template execution: %w", err)
	}

	// Also generate team associations
	for _, team := range teams {
		if _, err := os.Stat(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err == nil {
			if err = os.Remove(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
				log.Println("Unable to remove existing association symlink:", err.Error())
				return nil, fmt.Errorf("Unable to remove existing association symlink: %s", err.Error())
			}
		}
		if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
			log.Println("Unable to create association symlink:", err.Error())
			return nil, fmt.Errorf("Unable to create association symlink: %s", err.Error())
		}
	}

	return b.Bytes(), nil
}

func genDexPasswordTpl() ([]byte, error) {
	if teams, err := fic.GetTeams(); err != nil {
		return nil, err
	} else {
		b := bytes.NewBufferString("")

		if dexTmpl, err := template.New("dexpasswd").Parse(dexpasswdtpl); err != nil {
			return nil, fmt.Errorf("Cannot create template: %w", err)
		} else if err = dexTmpl.Execute(b, dexConfig{
			Teams: teams,
		}); err != nil {
			return nil, fmt.Errorf("An error occurs during template execution: %w", err)
		} else {
			return b.Bytes(), nil
		}
	}
}

const vouchcfgtpl = `# CONFIGURATION FILE HANDLED BY fic-admin
# DO NOT MODIFY IT BY HAND

vouch:
  logLevel: debug
  allowAllUsers: true
  document_root: /challenge_access

  cookie:
    domain: {{ .Domain }}

oauth:
  provider: oidc
  client_id: {{ .ClientId }}
  client_secret: {{ .ClientSecret }}
  callback_urls:
    - https://{{ .Domain }}/challenge_access/auth
  auth_url: https://{{ .Domain }}/auth
  token_url: http://127.0.0.1:5556/token
  user_info_url: http://127.0.0.1:5556/userinfo
  scopes:
    - openid
    - email
`

type vouchProxyConfig struct {
	Domain       string
	ClientId     string
	ClientSecret string
}

func genVouchProxyConfig() ([]byte, error) {
	if OidcSecret == "" {
		return nil, fmt.Errorf("Unable to generate vouch proxy configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
	}

	b := bytes.NewBufferString("")

	if vouchTmpl, err := template.New("vouchcfg").Parse(vouchcfgtpl); err != nil {
		return nil, fmt.Errorf("Cannot create template: %w", err)
	} else if err = vouchTmpl.Execute(b, vouchProxyConfig{
		Domain:       OidcIssuer,
		ClientId:     OidcClientId,
		ClientSecret: OidcSecret,
	}); err != nil {
		return nil, fmt.Errorf("An error occurs during template execution: %w", err)
	} else {
		return b.Bytes(), nil
	}
}
