package auth_connectors

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"oc-auth/conf"
	"oc-auth/infrastructure/claims"
	"regexp"
	"strconv"
	"strings"
	"time"

	oclib "cloud.o-forge.io/core/oc-lib"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"cloud.o-forge.io/core/oc-lib/tools"
)

type HydraConnector struct {
	State        string `json:"state"`
	Scopes       string `json:"scope"`
	ClientID     string `json:"client_id"`
	ResponseType string `json:"response_type"`

	Caller *tools.HTTPCaller
}

const test_name = "test-pierre"
const test_id = "1234"

func (a HydraConnector) Status() tools.State {
	caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{})
	var responseBody map[string]interface{}
	host := conf.GetConfig().AuthConnectorHost
	port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort)
	resp, err := caller.CallGet("http://"+host+":"+port, "/health/ready")
	if err != nil {
		return tools.DEAD
	}
	err = json.Unmarshal(resp, &responseBody)
	if err != nil || responseBody["status"] != "ok" {
		return tools.DEAD
	}
	return tools.ALIVE
}


// urlFormat formats the URL of the peer with the data type API function
func (a *HydraConnector) urlFormat(url string, replaceWith string) string {
	// localhost is replaced by the local peer URL
	// because localhost must collide on a web request security protocol
	r := regexp.MustCompile("(http://[a-z]+:[0-9]+)/oauth2")
	t := r.FindString(url)
	if t != "" {
		url = strings.Replace(url, t, replaceWith, -1)
	}
	return url
}

func (a HydraConnector) challenge(username string, url string, challenge string, cookies ...*http.Cookie) (*Redirect, string, []*http.Cookie, error) {
	body := map[string]interface{}{
		"remember_for": 0,
		"remember":     true,
	}
	if challenge != "consent" {
		body["subject"] = username
	}
	s := strings.Split(url, challenge+"_challenge=")
	resp, err := a.Caller.CallRaw(http.MethodPut,
		a.getPath(true, true), "/auth/requests/"+challenge+"/accept?"+challenge+"_challenge="+s[1],
		body, "application/json", true, cookies...) // "remember": true, "subject": username
	if err != nil {
		return nil, s[1], cookies, err
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, s[1], cookies, err
	}
	var token Redirect
	err = json.Unmarshal(b, &token)
	if err != nil {
		return nil, s[1], cookies, err
	}
	return &token, s[1], cookies, nil
}

func (a HydraConnector) Refresh(token *Token) (*Token, error) {
	access := strings.Split(token.AccessToken, ".")
	if len(access) > 2 {
		token.AccessToken = strings.Join(access[0:2], ".")
	}
	isValid, err := a.Introspect(token.AccessToken)
	if err != nil || !isValid {
		return nil, err
	}
	_, err = a.Logout(token.AccessToken)
	if err != nil {
		return nil, err
	}
	return a.Login(token.Username)
}

func (a HydraConnector) tryLog(username string, url string, subpath string, challenge string, cookies ...*http.Cookie) (*Redirect, string, []*http.Cookie, error) {
	resp, err := a.Caller.CallRaw(http.MethodGet, url, subpath,
		map[string]interface{}{}, "application/json", true, cookies...)
	if err != nil || resp.Request.Response == nil || resp.Request.Response.Header["Set-Cookie"] == nil {
		return nil, "", cookies, err
	}
	cc := resp.Request.Response.Header["Set-Cookie"] // retrieve oauth2 csrf token cookie
	if len(cc) > 0 {
		for _, c := range cc {
			first := strings.Split(c, ";")
			cookies = append(cookies, &http.Cookie{
				Name:  strings.Split(first[0], "=")[0],
				Value: strings.ReplaceAll(first[0], strings.Split(first[0], "=")[0]+"=", ""),
			})
		}
	}
	return a.challenge(username, resp.Request.URL.String(), challenge, cookies...)
}

func (a HydraConnector) getClient() string {
	resp, err := a.Caller.CallGet(a.getPath(true, false), "/clients")
	if err != nil {
		return ""
	}
	var clients []interface{}
	err = json.Unmarshal(resp, &clients)
	if err != nil || len(clients) == 0 {
		return ""
	}
	return clients[0].(map[string]interface{})["client_id"].(string)
}

func (a HydraConnector) Login(username string, cookies ...*http.Cookie) (t *Token, err error) {
	clientID := a.getClient()
	redirect, _, cookies, err := a.tryLog(username, a.getPath(false, true),
		"/auth?client_id="+clientID+"&response_type="+strings.ReplaceAll(a.ResponseType, " ", "%20")+"&scope="+strings.ReplaceAll(a.Scopes, " ", "%20")+"&state="+a.State,
		"login", cookies...)
	if err != nil || redirect == nil {
		return nil, err
	}
	redirect, _, cookies, err = a.tryLog(username, a.urlFormat(redirect.RedirectTo, a.getPath(false, true)), "", "consent", cookies...)
	if err != nil || redirect == nil {
		return nil, err
	}
	// problem with consent THERE we need to accept the consent challenge && get the token
	_, err = a.Caller.CallRaw(http.MethodGet, a.urlFormat(redirect.RedirectTo, a.getPath(false, true)), "", map[string]interface{}{},
		"application/json", true, cookies...)
	if err != nil {
		s := strings.Split(err.Error(), "\"")
		if len(s) > 1 && strings.Contains(s[1], "access_token") {
			err = nil
		} else {
			return nil, err
		}
	}
	token := &Token{
		Username: username,
	}
	urls := url.Values{}
	urls.Add("client_id", clientID)
	urls.Add("client_secret", conf.GetConfig().ClientSecret)
	urls.Add("grant_type", "client_credentials")
	resp, err := a.Caller.CallForm(http.MethodPost, a.getPath(false, true), "/token", urls,
		"application/x-www-form-urlencoded", true, cookies...)
	var m map[string]interface{}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(b, &token)
	if err != nil {
		return nil, err
	}
	json.Unmarshal(b, &m)
	pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER),test_name,test_id,nil,nil).Search(nil, strconv.Itoa(peer.SELF.EnumIndex()))
	if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
		return nil, errors.New("peer not found")
	}
	now := time.Now().UTC()
	now = now.Add(time.Duration(token.ExpiresIn) * time.Second)
	unix := now.Unix()

	c := claims.GetClaims().AddClaimsToToken(username, pp.Data[0].(*peer.Peer))
	c.Session.AccessToken["exp"] = unix

	b, _ = json.Marshal(c)

	token.AccessToken = strings.ReplaceAll(token.AccessToken, "ory_at_", "") + "." + base64.StdEncoding.EncodeToString(b)
	token.Active = true
	return token, nil
}

func (a HydraConnector) Logout(token string, cookies ...*http.Cookie) (*Token, error) {
	access := strings.Split(token, ".")
	if len(access) > 2 {
		token = strings.Join(access[0:2], ".")
	}
	p := a.getPath(false, true) + "/revoke"
	urls := url.Values{}
	urls.Add("token", token)
	urls.Add("client_id", a.getClient())
	urls.Add("client_secret", conf.GetConfig().ClientSecret)
	_, err := a.Caller.CallForm(http.MethodPost, p, "", urls, "application/x-www-form-urlencoded", true)
	if err != nil {
		return nil, err
	}
	return &Token{
		AccessToken: token,
		Active:      false,
	}, nil
}
func (a HydraConnector) Introspect(token string, cookie ...*http.Cookie) (bool, error) {
	// check validity of the token by calling introspect endpoint
	// if token is not active, we need to re-authenticate by sending the user to the login page
	access := strings.Split(token, ".")
	if len(access) > 2 {
		token = strings.Join(access[0:2], ".")
	}
	urls := url.Values{}
	urls.Add("token", token)
	resp, err := a.Caller.CallForm(http.MethodPost, a.getPath(true, true), "/introspect", urls,
		"application/x-www-form-urlencoded", true, cookie...)
	if err != nil || resp.StatusCode >= 300 {
		return false, err
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return false, err
	}
	var introspect Token
	err = json.Unmarshal(b, &introspect)
	if err != nil {
		return false, err
	}
	introspect.AccessToken = token
	return introspect.Active, nil
}

func (a HydraConnector) getPath(isAdmin bool, isOauth bool) string {
	host := conf.GetConfig().AuthConnectorHost
	port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort)
	if isAdmin {
		port = fmt.Sprintf("%v", conf.GetConfig().AuthConnectorAdminPort) + "/admin"
	}
	oauth := ""
	if isOauth {
		oauth = "/oauth2"
	}
	return "http://" + host + ":" + port + oauth

}

func (a HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool {
	if forward == "" || method == "" {
		return false
	}
	var c claims.Claims
	token := strings.Split(reqToken, ".")
	if len(token) > 2 {
		bytes, err := base64.StdEncoding.DecodeString(token[2])
		if err != nil {
			return false
		}
		err = json.Unmarshal(bytes, &c)
		if err != nil {
			return false
		}
	}
	// ask keto for permission is in claims
	ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, c, publicKey, external)
	if err != nil {
		fmt.Println("Failed to decode claims", err)
	}
	return ok
}
