package irc

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

	"github.com/ergochat/ergo/irc/modes"
	"github.com/ergochat/ergo/irc/sno"
	"github.com/ergochat/ergo/irc/utils"
)

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

	// server-level functionality:
	api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
	api.mux.HandleFunc("POST /v1/status", api.handleStatus)
	api.mux.HandleFunc("POST /v1/list", api.handleList)
	api.mux.HandleFunc("POST /v1/defcon", api.handleDefcon)

	// use Ergo as a source of truth for authentication in other services:
	api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)

	// legacy names for /v1/ns endpoints:
	api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
	api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
	api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList)

	// /v1/ns: nickserv functionality
	api.mux.HandleFunc("POST /v1/ns/info", api.handleAccountDetails)
	api.mux.HandleFunc("POST /v1/ns/list", api.handleAccountList)
	api.mux.HandleFunc("POST /v1/ns/passwd", api.handleNsPasswd)
	api.mux.HandleFunc("POST /v1/ns/saregister", api.handleSaregister)

	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 defconRequestResponse struct {
	apiGenericResponse
	Defcon int `json:"defcon"`
}

func (a *ergoAPI) handleDefcon(w http.ResponseWriter, r *http.Request) {
	var changeRequested uint32
	var request defconRequestResponse
	// ignore errors or invalid values
	if err := json.NewDecoder(r.Body).Decode(&request); err == nil {
		if 1 <= request.Defcon && request.Defcon <= 5 {
			changeRequested = uint32(request.Defcon)
		}
	}
	if changeRequested != 0 {
		a.server.SetDefcon(changeRequested)
		message := fmt.Sprintf("API set DEFCON level to %d", changeRequested)
		a.server.logger.Info("server", message)
		a.server.snomasks.Send(sno.LocalAnnouncements, message)
	}
	a.writeJSONResponse(
		defconRequestResponse{
			apiGenericResponse: apiGenericResponse{Success: true},
			Defcon:             int(a.server.Defcon()),
		}, 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

	var account ClientAccount
	var err error

	// try whatever credentials are present
	if request.AccountName != "" && request.Passphrase != "" {
		account, err = a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
	} else if request.Certfp != "" {
		account, err = a.server.accounts.checkCertAuth(nil, request.Certfp, nil, "")
	} else {
		err = errAccountInvalidCredentials
	}

	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()
	}

	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)
}

func (a *ergoAPI) handleNsPasswd(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.setPassword(request.AccountName, request.Passphrase, true)
	switch err {
	case nil:
		response.Success = true
	case errAccountDoesNotExist:
		response.ErrorCode = "ACCOUNT_DOES_NOT_EXIST"
	case errAccountBadPassphrase, errEmptyCredentials:
		response.ErrorCode = "INVALID_PASSPHRASE"
	case errCredsExternallyManaged:
		response.ErrorCode = "CREDENTIALS_EXTERNALLY_MANAGED"
	default:
		a.server.logger.Error("api", "could not change user password:", err.Error())
		response.ErrorCode = "UNKNOWN_ERROR"
	}

	a.writeJSONResponse(response, w, r)
}

type apiAccountDetailsResponse struct {
	apiGenericResponse
	AccountName  string   `json:"accountName,omitempty"`
	Email        string   `json:"email,omitempty"`
	RegisteredAt string   `json:"registeredAt,omitempty"`
	Channels     []string `json:"channels,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

	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
			if !accountData.RegisteredAt.IsZero() {
				response.RegisteredAt = accountData.RegisteredAt.Format(utils.IRCv3TimestampFormat)
			}

			// Get channels the account is in
			response.Channels = a.server.channels.ChannelsForAccount(accountData.NameCasefolded)
			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)
}

type apiAccountListResponse struct {
	apiGenericResponse
	Accounts   []apiAccountDetailsResponse `json:"accounts"`
	TotalCount int                         `json:"totalCount"`
}

func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) {
	var response apiAccountListResponse

	// Get all account names
	accounts := a.server.accounts.AllNicks()
	response.TotalCount = len(accounts)

	// Load account details
	response.Accounts = make([]apiAccountDetailsResponse, 0, len(accounts))
	for _, account := range accounts {
		accountData, err := a.server.accounts.LoadAccount(account)
		if err != nil {
			// shouldn't happen
			continue
		}

		response.Accounts = append(
			response.Accounts,
			apiAccountDetailsResponse{
				apiGenericResponse: apiGenericResponse{
					Success: true,
				},
				AccountName: accountData.Name,
				Email:       accountData.Settings.Email,
			},
		)
	}

	response.Success = true
	a.writeJSONResponse(response, w, r)
}

type apiStatusResponse struct {
	apiGenericResponse
	Version   string `json:"version"`
	GoVersion string `json:"go_version"`
	Commit    string `json:"commit,omitempty"`
	StartTime string `json:"start_time"`
	Users     struct {
		Total     int `json:"total"`
		Invisible int `json:"invisible"`
		Operators int `json:"operators"`
		Unknown   int `json:"unknown"`
		Max       int `json:"max"`
	} `json:"users"`
	Channels int `json:"channels"`
	Servers  int `json:"servers"`
}

func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) {
	server := a.server
	stats := server.stats.GetValues()

	response := apiStatusResponse{
		apiGenericResponse: apiGenericResponse{Success: true},
		Version:            SemVer,
		GoVersion:          runtime.Version(),
		Commit:             Commit,
		StartTime:          server.ctime.Format(utils.IRCv3TimestampFormat),
	}

	response.Users.Total = stats.Total
	response.Users.Invisible = stats.Invisible
	response.Users.Operators = stats.Operators
	response.Users.Unknown = stats.Unknown
	response.Users.Max = stats.Max
	response.Channels = server.channels.Len()
	response.Servers = 1

	a.writeJSONResponse(response, w, r)
}

type apiChannelData struct {
	Name         string `json:"name"`
	HasKey       bool   `json:"hasKey"`
	InviteOnly   bool   `json:"inviteOnly"`
	Secret       bool   `json:"secret"`
	UserCount    int    `json:"userCount"`
	Topic        string `json:"topic"`
	TopicSetAt   string `json:"topicSetAt,omitempty"`
	CreatedAt    string `json:"createdAt"`
	Registered   bool   `json:"registered"`
	Owner        string `json:"owner,omitempty"`
	RegisteredAt string `json:"registeredAt,omitempty"`
}

func (channel *Channel) apiData() (result apiChannelData) {
	channel.stateMutex.RLock()
	defer channel.stateMutex.RUnlock()
	result.Name = channel.name
	result.HasKey = channel.key != ""
	result.InviteOnly = channel.flags.HasMode(modes.InviteOnly)
	result.Secret = channel.flags.HasMode(modes.Secret)
	result.UserCount = len(channel.members)
	result.Topic = channel.topic
	if !channel.topicSetTime.IsZero() {
		result.TopicSetAt = channel.topicSetTime.UTC().Format(utils.IRCv3TimestampFormat)
	}
	result.CreatedAt = channel.createdTime.UTC().Format(utils.IRCv3TimestampFormat)
	result.Registered = channel.registeredFounder != ""
	if result.Registered {
		result.Owner = channel.registeredFounder
		if !channel.registeredTime.IsZero() {
			result.RegisteredAt = channel.registeredTime.UTC().Format(utils.IRCv3TimestampFormat)
		}
	}
	return
}

type apiListResponse struct {
	apiGenericResponse
	Channels []apiChannelData `json:"channels"`
}

func (a *ergoAPI) handleList(w http.ResponseWriter, r *http.Request) {
	channels := a.server.channels.ListableChannels()
	response := apiListResponse{
		apiGenericResponse: apiGenericResponse{Success: true},
		Channels:           make([]apiChannelData, 0, len(channels)),
	}
	for _, channel := range channels {
		response.Channels = append(response.Channels, channel.apiData())
	}
	a.writeJSONResponse(response, w, r)
}
