// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

//go:build gssapi && (linux || darwin)
// +build gssapi
// +build linux darwin

package gssapi

/*
#cgo linux CFLAGS: -DGOOS_linux
#cgo linux LDFLAGS: -lgssapi_krb5 -lkrb5
#cgo darwin CFLAGS: -DGOOS_darwin
#cgo darwin LDFLAGS: -framework GSS
#include "gss_wrapper.h"
*/
import "C"

import (
	"context"
	"fmt"
	"runtime"
	"strings"
	"unsafe"
)

// New creates a new SaslClient. The target parameter should be a hostname with no port.
func New(target, username, password string, passwordSet bool, props map[string]string) (*SaslClient, error) {
	serviceName := "mongodb"

	for key, value := range props {
		switch strings.ToUpper(key) {
		case "CANONICALIZE_HOST_NAME":
			return nil, fmt.Errorf("CANONICALIZE_HOST_NAME is not supported when using gssapi on %s", runtime.GOOS)
		case "SERVICE_REALM":
			return nil, fmt.Errorf("SERVICE_REALM is not supported when using gssapi on %s", runtime.GOOS)
		case "SERVICE_NAME":
			serviceName = value
		case "SERVICE_HOST":
			target = value
		default:
			return nil, fmt.Errorf("unknown mechanism property %s", key)
		}
	}

	servicePrincipalName := fmt.Sprintf("%s@%s", serviceName, target)

	return &SaslClient{
		servicePrincipalName: servicePrincipalName,
		username:             username,
		password:             password,
		passwordSet:          passwordSet,
	}, nil
}

type SaslClient struct {
	servicePrincipalName string
	username             string
	password             string
	passwordSet          bool

	// state
	state           C.gssapi_client_state
	contextComplete bool
	done            bool
}

func (sc *SaslClient) Close() {
	C.gssapi_client_destroy(&sc.state)
}

func (sc *SaslClient) Start() (string, []byte, error) {
	const mechName = "GSSAPI"

	cservicePrincipalName := C.CString(sc.servicePrincipalName)
	defer C.free(unsafe.Pointer(cservicePrincipalName))
	var cusername *C.char
	var cpassword *C.char
	if sc.username != "" {
		cusername = C.CString(sc.username)
		defer C.free(unsafe.Pointer(cusername))
		if sc.passwordSet {
			cpassword = C.CString(sc.password)
			defer C.free(unsafe.Pointer(cpassword))
		}
	}
	status := C.gssapi_client_init(&sc.state, cservicePrincipalName, cusername, cpassword)

	if status != C.GSSAPI_OK {
		return mechName, nil, sc.getError("unable to initialize client")
	}

	payload, err := sc.Next(nil, nil)

	return mechName, payload, err
}

func (sc *SaslClient) Next(_ context.Context, challenge []byte) ([]byte, error) {
	var buf unsafe.Pointer
	var bufLen C.size_t
	var outBuf unsafe.Pointer
	var outBufLen C.size_t

	if sc.contextComplete {
		if sc.username == "" {
			var cusername *C.char
			status := C.gssapi_client_username(&sc.state, &cusername)
			if status != C.GSSAPI_OK {
				return nil, sc.getError("unable to acquire username")
			}
			defer C.free(unsafe.Pointer(cusername))
			sc.username = C.GoString((*C.char)(unsafe.Pointer(cusername)))
		}

		bytes := append([]byte{1, 0, 0, 0}, []byte(sc.username)...)
		buf = unsafe.Pointer(&bytes[0])
		bufLen = C.size_t(len(bytes))
		status := C.gssapi_client_wrap_msg(&sc.state, buf, bufLen, &outBuf, &outBufLen)
		if status != C.GSSAPI_OK {
			return nil, sc.getError("unable to wrap authz")
		}

		sc.done = true
	} else {
		if len(challenge) > 0 {
			buf = unsafe.Pointer(&challenge[0])
			bufLen = C.size_t(len(challenge))
		}

		status := C.gssapi_client_negotiate(&sc.state, buf, bufLen, &outBuf, &outBufLen)
		switch status {
		case C.GSSAPI_OK:
			sc.contextComplete = true
		case C.GSSAPI_CONTINUE:
		default:
			return nil, sc.getError("unable to negotiate with server")
		}
	}

	if outBuf != nil {
		defer C.free(outBuf)
	}

	return C.GoBytes(outBuf, C.int(outBufLen)), nil
}

func (sc *SaslClient) Completed() bool {
	return sc.done
}

func (sc *SaslClient) getError(prefix string) error {
	var desc *C.char

	status := C.gssapi_error_desc(sc.state.maj_stat, sc.state.min_stat, &desc)
	if status != C.GSSAPI_OK {
		if desc != nil {
			C.free(unsafe.Pointer(desc))
		}

		return fmt.Errorf("%s: (%v, %v)", prefix, sc.state.maj_stat, sc.state.min_stat)
	}
	defer C.free(unsafe.Pointer(desc))

	return fmt.Errorf("%s: %v(%v,%v)", prefix, C.GoString(desc), int32(sc.state.maj_stat), int32(sc.state.min_stat))
}
