// +build darwin,!ios

package keychain

/*
#cgo LDFLAGS: -framework CoreFoundation -framework Security

#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
import "C"
import (
	"os"
	"unsafe"
)

// AccessibleKey is key for kSecAttrAccessible
var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible))
var accessibleTypeRef = map[Accessible]C.CFTypeRef{
	AccessibleWhenUnlocked:                   C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked),
	AccessibleAfterFirstUnlock:               C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock),
	AccessibleAlways:                         C.CFTypeRef(C.kSecAttrAccessibleAlways),
	AccessibleWhenUnlockedThisDeviceOnly:     C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
	AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),
	AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly),

	// Only available in 10.10
	//AccessibleWhenPasscodeSetThisDeviceOnly:  C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
}

// DeleteItemRef deletes a keychain item reference.
func DeleteItemRef(ref C.CFTypeRef) error {
	errCode := C.SecKeychainItemDelete(C.SecKeychainItemRef(ref))
	return checkError(errCode)
}

var (
	// KeychainKey is key for kSecUseKeychain
	KeychainKey = attrKey(C.CFTypeRef(C.kSecUseKeychain))
	// MatchSearchListKey is key for kSecMatchSearchList
	MatchSearchListKey = attrKey(C.CFTypeRef(C.kSecMatchSearchList))
)

// Keychain represents the path to a specific OSX keychain
type Keychain struct {
	path string
}

// NewKeychain creates a new keychain file with a password
func NewKeychain(path string, password string) (Keychain, error) {
	return newKeychain(path, password, false)
}

// NewKeychainWithPrompt creates a new Keychain and prompts user for password
func NewKeychainWithPrompt(path string) (Keychain, error) {
	return newKeychain(path, "", true)
}

func newKeychain(path, password string, promptUser bool) (Keychain, error) {
	pathRef := C.CString(path)
	defer C.free(unsafe.Pointer(pathRef))

	var errCode C.OSStatus
	var kref C.SecKeychainRef

	if promptUser {
		errCode = C.SecKeychainCreate(pathRef, C.UInt32(0), nil, C.Boolean(1), 0, &kref) //nolint
	} else {
		passwordRef := C.CString(password)
		defer C.free(unsafe.Pointer(passwordRef))
		errCode = C.SecKeychainCreate(pathRef, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(0), 0, &kref) //nolint
	}

	if err := checkError(errCode); err != nil {
		return Keychain{}, err
	}

	// TODO: Without passing in kref I get 'One or more parameters passed to the function were not valid (-50)'
	defer Release(C.CFTypeRef(kref))

	return Keychain{
		path: path,
	}, nil
}

// NewWithPath to use an existing keychain
func NewWithPath(path string) Keychain {
	return Keychain{
		path: path,
	}
}

// Status returns the status of the keychain
func (kc Keychain) Status() error {
	// returns no error even if it doesn't exist
	kref, err := openKeychainRef(kc.path)
	if err != nil {
		return err
	}
	defer C.CFRelease(C.CFTypeRef(kref))

	var status C.SecKeychainStatus
	return checkError(C.SecKeychainGetStatus(kref, &status))
}

// The returned SecKeychainRef, if non-nil, must be released via CFRelease.
func openKeychainRef(path string) (C.SecKeychainRef, error) {
	pathName := C.CString(path)
	defer C.free(unsafe.Pointer(pathName))

	var kref C.SecKeychainRef
	if err := checkError(C.SecKeychainOpen(pathName, &kref)); err != nil { //nolint
		return 0, err
	}

	return kref, nil
}

// UnlockAtPath unlocks keychain at path
func UnlockAtPath(path string, password string) error {
	kref, err := openKeychainRef(path)
	defer Release(C.CFTypeRef(kref))
	if err != nil {
		return err
	}
	passwordRef := C.CString(password)
	defer C.free(unsafe.Pointer(passwordRef))
	return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1)))
}

// LockAtPath locks keychain at path
func LockAtPath(path string) error {
	kref, err := openKeychainRef(path)
	defer Release(C.CFTypeRef(kref))
	if err != nil {
		return err
	}
	return checkError(C.SecKeychainLock(kref))
}

// Delete the Keychain
func (kc *Keychain) Delete() error {
	return os.Remove(kc.path)
}

// Convert Keychain to CFTypeRef.
// The returned CFTypeRef, if non-nil, must be released via CFRelease.
func (kc Keychain) Convert() (C.CFTypeRef, error) {
	keyRef, err := openKeychainRef(kc.path)
	return C.CFTypeRef(keyRef), err
}

type keychainArray []Keychain

// Convert the keychainArray to a CFTypeRef.
// The returned CFTypeRef, if non-nil, must be released via CFRelease.
func (ka keychainArray) Convert() (C.CFTypeRef, error) {
	var refs = make([]C.CFTypeRef, len(ka))
	var err error

	for idx, kc := range ka {
		if refs[idx], err = kc.Convert(); err != nil {
			// If we error trying to convert lets release any we converted before
			for _, ref := range refs {
				if ref != 0 {
					Release(ref)
				}
			}
			return 0, err
		}
	}

	return C.CFTypeRef(ArrayToCFArray(refs)), nil
}

// SetMatchSearchList sets match type on keychains
func (k *Item) SetMatchSearchList(karr ...Keychain) {
	k.attr[MatchSearchListKey] = keychainArray(karr)
}

// UseKeychain tells item to use the specified Keychain
func (k *Item) UseKeychain(kc Keychain) {
	k.attr[KeychainKey] = kc
}
