package irc

import (
	"crypto/subtle"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
)

func newAPIHandler(server *Server) http.Handler {
	api := &ergoAPI{
		server: server,
		mux:    http.NewServeMux(),
	}

	api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
	api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
	api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
	api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)

	return api
}

type ergoAPI struct {
	server *Server
	mux    *http.ServeMux
}

func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	defer a.server.HandlePanic(nil)

	defer a.server.logger.Debug("api", r.URL.Path)

	if a.checkBearerAuth(r.Header.Get("Authorization")) {
		a.mux.ServeHTTP(w, r)
	} else {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
	}
}

func (a *ergoAPI) checkBearerAuth(authHeader string) (authorized bool) {
	if authHeader == "" {
		return false
	}
	c := a.server.Config()
	if !c.API.Enabled {
		return false
	}
	spaceIdx := strings.IndexByte(authHeader, ' ')
	if spaceIdx < 0 {
		return false
	}
	if !strings.EqualFold("Bearer", authHeader[:spaceIdx]) {
		return false
	}
	providedTokenBytes := []byte(authHeader[spaceIdx+1:])
	for _, tokenBytes := range c.API.bearerTokenBytes {
		if subtle.ConstantTimeCompare(tokenBytes, providedTokenBytes) == 1 {
			return true
		}
	}
	return false
}

func (a *ergoAPI) decodeJSONRequest(request any, w http.ResponseWriter, r *http.Request) (err error) {
	err = json.NewDecoder(r.Body).Decode(request)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to deserialize json request: %v", err), http.StatusBadRequest)
	}
	return err
}

func (a *ergoAPI) writeJSONResponse(response any, w http.ResponseWriter, r *http.Request) {
	j, err := json.Marshal(response)
	if err == nil {
		j = append(j, '\n') // less annoying in curl output
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write(j)
	} else {
		a.server.logger.Error("internal", "failed to serialize API response", r.URL.Path, err.Error())
		http.Error(w, fmt.Sprintf("failed to serialize json response: %v", err), http.StatusInternalServerError)
	}
}

type apiGenericResponse struct {
	Success   bool   `json:"success"`
	Error     string `json:"error,omitempty"`
	ErrorCode string `json:"errorCode,omitempty"`
}

func (a *ergoAPI) handleRehash(w http.ResponseWriter, r *http.Request) {
	var response apiGenericResponse
	err := a.server.rehash()
	if err == nil {
		response.Success = true
	} else {
		response.Success = false
		response.Error = err.Error()
	}
	a.writeJSONResponse(response, w, r)
}

type apiCheckAuthResponse struct {
	apiGenericResponse
	AccountName string `json:"accountName,omitempty"`
}

func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
	var request AuthScriptInput
	if err := a.decodeJSONRequest(&request, w, r); err != nil {
		return
	}

	var response apiCheckAuthResponse

	// try passphrase if present
	if request.AccountName != "" && request.Passphrase != "" {
		// TODO this only checks the internal database, not auth-script;
		// it's a little weird to use both auth-script and the API but we should probably handle it
		account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
		switch err {
		case nil:
			// success, no error
			response.Success = true
			response.AccountName = account.Name
		case errAccountDoesNotExist, errAccountInvalidCredentials, errAccountUnverified, errAccountSuspended:
			// fail, no error
			response.Success = false
		default:
			response.Success = false
			response.Error = err.Error()
		}
	}

	// try certfp if present
	if !response.Success && request.Certfp != "" {
		// TODO support cerftp
	}

	a.writeJSONResponse(response, w, r)
}

type apiSaregisterRequest struct {
	AccountName string `json:"accountName"`
	Passphrase  string `json:"passphrase"`
}

func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) {
	var request apiSaregisterRequest
	if err := a.decodeJSONRequest(&request, w, r); err != nil {
		return
	}

	var response apiGenericResponse
	err := a.server.accounts.SARegister(request.AccountName, request.Passphrase)
	if err == nil {
		response.Success = true
	} else {
		response.Success = false
		response.Error = err.Error()
		switch err {
		case errAccountAlreadyRegistered, errAccountAlreadyVerified, errNameReserved:
			response.ErrorCode = "ACCOUNT_EXISTS"
		case errAccountBadPassphrase:
			response.ErrorCode = "INVALID_PASSPHRASE"
		default:
			response.ErrorCode = "UNKNOWN_ERROR"
		}
	}

	a.writeJSONResponse(response, w, r)
}

type apiAccountDetailsResponse struct {
	apiGenericResponse
	AccountName string `json:"accountName,omitempty"`
	Email       string `json:"email,omitempty"`
}

type apiAccountDetailsRequest struct {
	AccountName string `json:"accountName"`
}

func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
	var request apiAccountDetailsRequest
	if err := a.decodeJSONRequest(&request, w, r); err != nil {
		return
	}

	var response apiAccountDetailsResponse

	// TODO could probably use better error handling and more details

	if request.AccountName != "" {
		accountData, err := a.server.accounts.LoadAccount(request.AccountName)
		if err == nil {
			if !accountData.Verified {
				err = errAccountUnverified
			} else if accountData.Suspended != nil {
				err = errAccountSuspended
			}
		}

		switch err {
		case nil:
			response.AccountName = accountData.Name
			response.Email = accountData.Settings.Email
			response.Success = true
		case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
			response.Success = false
		default:
			response.Success = false
			response.ErrorCode = "UNKNOWN_ERROR"
			response.Error = err.Error()
		}
	} else {
		response.Success = false
		response.ErrorCode = "INVALID_REQUEST"
	}

	a.writeJSONResponse(response, w, r)
}