// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package store

import (
	"crypto/sha1"
	"encoding/json"
	"errors"
	"fmt"
	"sync"

	"github.com/99designs/keyring"
	"github.com/apache/pulsar-client-go/oauth2"
	"github.com/apache/pulsar-client-go/oauth2/clock"
)

type KeyringStore struct {
	kr    keyring.Keyring
	clock clock.Clock
	lock  sync.Mutex
}

// storedItem represents an item stored in the keyring
type storedItem struct {
	Audience string
	UserName string
	Grant    oauth2.AuthorizationGrant
}

// NewKeyringStore creates a store based on a keyring.
func NewKeyringStore(kr keyring.Keyring) (*KeyringStore, error) {
	return &KeyringStore{
		kr:    kr,
		clock: clock.RealClock{},
	}, nil
}

var _ Store = &KeyringStore{}

func (f *KeyringStore) SaveGrant(audience string, grant oauth2.AuthorizationGrant) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	var err error
	var userName string
	switch grant.Type {
	case oauth2.GrantTypeClientCredentials:
		if grant.ClientCredentials == nil {
			return ErrUnsupportedAuthData
		}
		userName = grant.ClientCredentials.ClientEmail
	case oauth2.GrantTypeDeviceCode:
		if grant.Token == nil {
			return ErrUnsupportedAuthData
		}
		userName, err = oauth2.ExtractUserName(*grant.Token)
		if err != nil {
			return err
		}
	default:
		return ErrUnsupportedAuthData
	}
	item := storedItem{
		Audience: audience,
		UserName: userName,
		Grant:    grant,
	}
	err = f.setItem(item)
	if err != nil {
		return err
	}
	return nil
}

func (f *KeyringStore) LoadGrant(audience string) (*oauth2.AuthorizationGrant, error) {
	f.lock.Lock()
	defer f.lock.Unlock()

	item, err := f.getItem(audience)
	if err != nil {
		if errors.Is(err, keyring.ErrKeyNotFound) {
			return nil, ErrNoAuthenticationData
		}
		return nil, err
	}
	switch item.Grant.Type {
	case oauth2.GrantTypeClientCredentials:
		if item.Grant.ClientCredentials == nil {
			return nil, ErrUnsupportedAuthData
		}
	case oauth2.GrantTypeDeviceCode:
		if item.Grant.Token == nil {
			return nil, ErrUnsupportedAuthData
		}
	default:
		return nil, ErrUnsupportedAuthData
	}
	return &item.Grant, nil
}

func (f *KeyringStore) WhoAmI(audience string) (string, error) {
	f.lock.Lock()
	defer f.lock.Unlock()

	key := hashKeyringKey(audience)
	authItem, err := f.kr.Get(key)
	if err != nil {
		if errors.Is(err, keyring.ErrKeyNotFound) {
			return "", ErrNoAuthenticationData
		}
		return "", fmt.Errorf("unable to get information from the keyring: %w", err)
	}
	return authItem.Label, nil
}

func (f *KeyringStore) Logout() error {
	f.lock.Lock()
	defer f.lock.Unlock()

	var err error
	keys, err := f.kr.Keys()
	if err != nil {
		return fmt.Errorf("unable to get information from the keyring: %w", err)
	}
	for _, key := range keys {
		err = f.kr.Remove(key)
	}
	if err != nil {
		return fmt.Errorf("unable to update the keyring: %w", err)
	}
	return nil
}

func (f *KeyringStore) getItem(audience string) (storedItem, error) {
	key := hashKeyringKey(audience)
	i, err := f.kr.Get(key)
	if err != nil {
		return storedItem{}, err
	}
	var grant oauth2.AuthorizationGrant
	err = json.Unmarshal(i.Data, &grant)
	if err != nil {
		// the grant appears to be invalid
		return storedItem{}, ErrUnsupportedAuthData
	}
	return storedItem{
		Audience: audience,
		UserName: i.Label,
		Grant:    grant,
	}, nil
}

func (f *KeyringStore) setItem(item storedItem) error {
	key := hashKeyringKey(item.Audience)
	data, err := json.Marshal(item.Grant)
	if err != nil {
		return err
	}
	i := keyring.Item{
		Key:                         key,
		Data:                        data,
		Label:                       item.UserName,
		Description:                 "authorization grant",
		KeychainNotTrustApplication: false,
		KeychainNotSynchronizable:   false,
	}
	err = f.kr.Set(i)
	if err != nil {
		return fmt.Errorf("unable to update the keyring: %w", err)
	}
	return nil
}

// hashKeyringKey creates a safe key based on the given string
func hashKeyringKey(s string) string {
	h := sha1.New()
	h.Write([]byte(s))
	bs := h.Sum(nil)
	return fmt.Sprintf("%x", bs)
}
