/*
Copyright © The ESO Authors

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

    https://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 passworddepot

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"
)

const (
	// DoRequestError is the error format string for request errors.
	DoRequestError = "error: do request: %w"
)

// HTTPClient is an interface representing the ability to perform HTTP requests.
type HTTPClient interface {
	Do(*http.Request) (*http.Response, error)
}

// AccessData represents the access credentials returned by the Password Depot API upon successful login.
type AccessData struct {
	ClientID    string `json:"client_id"`
	AccessToken string `json:"access_token"`
}

// Databases represents the list of Password Depot databases accessible with the current credentials.
type Databases struct {
	Databases []struct {
		Name         string    `json:"name"`
		Fingerprint  string    `json:"fingerprint"`
		Date         time.Time `json:"date"`
		Rights       string    `json:"rights"`
		Reasondelete string    `json:"reasondelete"`
	} `json:"databases"`
	Infoclasses          string `json:"infoclasses"`
	Policyforce          string `json:"policyforce"`
	Policyminlength      string `json:"policyminlength"`
	Policyincludeatleast string `json:"policyincludeatleast"`
	Policymingroups      string `json:"policymingroups"`
	Policyselectedgroups string `json:"policyselectedgroups"`
}

// DatabaseEntries represents the entries in a Password Depot database.
type DatabaseEntries struct {
	Name         string  `json:"name"`
	Parent       string  `json:"parent"`
	Entries      []Entry `json:"entries"`
	Infoclasses  string  `json:"infoclasses"`
	Reasondelete string  `json:"reasondelete"`
}

// Entry represents a single entry in the Password Depot database.
type Entry struct {
	Name        string    `json:"name"`
	Login       string    `json:"login"`
	Password    string    `json:"pass"`
	URL         string    `json:"url"`
	Importance  string    `json:"importance"`
	Date        time.Time `json:"date"`
	Icon        string    `json:"icon"`
	Secondeye   string    `json:"secondeye"`
	Fingerprint string    `json:"fingerprint"`
	Rights      string    `json:"rights"`
	Itemclass   string    `json:"itemclass"`
}

// API represents a client for the Password Depot API.
type API struct {
	client   HTTPClient
	baseURL  string
	hostPort string
	secret   *AccessData
	password string
	username string
}

// SecretEntry represents a secret entry in Password Depot.
type SecretEntry struct {
	Name        string    `json:"name"`
	Fingerprint string    `json:"fingerprint"`
	Itemclass   string    `json:"itemclass"`
	Login       string    `json:"login"`
	Pass        string    `json:"pass"`
	URL         string    `json:"url"`
	Importance  string    `json:"importance"`
	Date        time.Time `json:"date"`
	Comment     string    `json:"comment"`
	Expirydate  string    `json:"expirydate"`
	Tags        string    `json:"tags"`
	Author      string    `json:"author"`
	Category    string    `json:"category"`
	Icon        string    `json:"icon"`
	Secondeye   string    `json:"secondeye"`
	Secondpass  string    `json:"secondpass"`
	Template    string    `json:"template"`
	Acm         string    `json:"acm"`
	Paramstr    string    `json:"paramstr"`
	Loginid     string    `json:"loginid"`
	Passid      string    `json:"passid"`
	Donotaddon  string    `json:"donotaddon"`
	Markassafe  string    `json:"markassafe"`
	Safemode    string    `json:"safemode"`
}

var errDBNotFound = errors.New("database not found")
var errSecretNotFound = errors.New("secret not found")

// NewAPI creates a new instance of the PasswordDepot API client and performs login.
func NewAPI(ctx context.Context, baseURL, username, password, hostPort string) (*API, error) {
	api := &API{
		baseURL:  baseURL,
		hostPort: hostPort,
		username: username,
		password: password,
	}
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
	}

	api.client = &http.Client{Transport: tr}
	err := api.login(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to login: %w", err)
	}
	return api, nil
}

func (api *API) doAuthenticatedRequest(r *http.Request) (*http.Response, error) {
	r.Header.Add("access_token", api.secret.AccessToken)
	r.Header.Add("client_id", api.secret.ClientID)

	return api.client.Do(r)
}
func (api *API) getDatabaseFingerprint(database string) (string, error) {
	databases, err := api.ListDatabases()
	if err != nil {
		return "", fmt.Errorf("error: getting database list: %w", err)
	}

	for _, db := range databases.Databases {
		if strings.Contains(db.Name, database) {
			return db.Fingerprint, nil
		}
	}

	return "", errDBNotFound
}

func (api *API) getSecretFingerprint(databaseFingerprint, secretName, folder string) (string, error) {
	secrets, err := api.ListSecrets(databaseFingerprint, folder)
	if err != nil {
		return "", fmt.Errorf("error: getting secrets list: %w", err)
	}

	parts := strings.Split(secretName, ".")
	searchName := parts[0]
	var fingerprint string
	for _, entry := range secrets.Entries {
		if strings.Contains(entry.Name, searchName) {
			fingerprint = entry.Fingerprint
			if len(parts) > 1 {
				return api.getSecretFingerprint(databaseFingerprint, strings.Join(parts[1:], "."), fingerprint)
			}
			return fingerprint, nil
		}
	}
	return "", errSecretNotFound
}

func (api *API) getendpointURL(endpoint string) string {
	return fmt.Sprintf("https://%s:%s/v1.0/%s", api.baseURL, api.hostPort, endpoint)
}

func (api *API) login(ctx context.Context) error {
	loginRequest, err := http.NewRequestWithContext(ctx, "GET", api.getendpointURL("login"), http.NoBody)
	if err != nil {
		return fmt.Errorf("error creating request: %w", err)
	}
	loginRequest.Header.Add("user", api.username)
	loginRequest.Header.Add("pass", api.password)

	resp, err := api.client.Do(loginRequest) //nolint:bodyclose // linters bug
	if err != nil {
		return fmt.Errorf(DoRequestError, err)
	}

	accessData := AccessData{}
	err = ReadAndUnmarshal(resp, &accessData)
	if err != nil {
		return fmt.Errorf("error: failed to unmarshal response body: %w", err)
	}

	api.secret = &accessData

	return nil
}

// ListSecrets retrieves the list of secrets from the specified database and folder.
func (api *API) ListSecrets(dbFingerprint, folder string) (DatabaseEntries, error) {
	endpointURL := api.getendpointURL(fmt.Sprintf("list?db=%s", dbFingerprint))
	if folder != "" {
		endpointURL = fmt.Sprintf("%s&folder=%s", endpointURL, folder)
	}
	listSecrets, err := http.NewRequest("GET", endpointURL, http.NoBody)
	if err != nil {
		return DatabaseEntries{}, fmt.Errorf("error: creating secrets request: %w", err)
	}

	respSecretsList, err := api.doAuthenticatedRequest(listSecrets) //nolint:bodyclose // linters bug
	if err != nil {
		return DatabaseEntries{}, fmt.Errorf(DoRequestError, err)
	}

	dbEntries := DatabaseEntries{}
	err = ReadAndUnmarshal(respSecretsList, &dbEntries)
	return dbEntries, err
}

// ReadAndUnmarshal reads the response body and unmarshals it into the target struct.
func ReadAndUnmarshal(resp *http.Response, target any) error {
	var buf bytes.Buffer
	defer func() {
		_ = resp.Body.Close()
	}()
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("failed to authenticate with the given credentials: %d %s", resp.StatusCode, buf.String())
	}
	_, err := buf.ReadFrom(resp.Body)
	if err != nil {
		return err
	}
	return json.Unmarshal(buf.Bytes(), target)
}

// ListDatabases retrieves the list of databases accessible with the current credentials.
func (api *API) ListDatabases() (Databases, error) {
	listDBRequest, err := http.NewRequest("GET", api.getendpointURL("list"), http.NoBody)
	if err != nil {
		return Databases{}, fmt.Errorf("error: creating db request: %w", err)
	}

	respDBList, err := api.doAuthenticatedRequest(listDBRequest) //nolint:bodyclose // linters bug
	if err != nil {
		return Databases{}, fmt.Errorf(DoRequestError, err)
	}

	databases := Databases{}
	err = ReadAndUnmarshal(respDBList, &databases)
	return databases, err
}

// GetSecret retrieves a secret by its name from the specified database.
func (api *API) GetSecret(database, secretName string) (SecretEntry, error) {
	dbFingerprint, err := api.getDatabaseFingerprint(database)
	if err != nil {
		return SecretEntry{}, fmt.Errorf("error: getting DB fingerprint: %w", err)
	}

	secretFingerprint, err := api.getSecretFingerprint(dbFingerprint, secretName, "")
	if err != nil {
		return SecretEntry{}, fmt.Errorf("error: getting Secret fingerprint: %w", err)
	}
	readSecretRequest, err := http.NewRequest("GET", api.getendpointURL(fmt.Sprintf("read?db=%s&entry=%s", dbFingerprint, secretFingerprint)), http.NoBody)
	if err != nil {
		return SecretEntry{}, fmt.Errorf("error: creating secrets request: %w", err)
	}

	respSecretRead, err := api.doAuthenticatedRequest(readSecretRequest) //nolint:bodyclose // linters bug
	if err != nil {
		return SecretEntry{}, fmt.Errorf(DoRequestError, err)
	}

	secretEntry := SecretEntry{}
	err = ReadAndUnmarshal(respSecretRead, &secretEntry)
	return secretEntry, err
}

// ToMap converts the SecretEntry struct to a map[string][]byte.
func (s *SecretEntry) ToMap() map[string][]byte {
	m := make(map[string][]byte)

	m["name"] = []byte(s.Name)
	m["fingerprint"] = []byte(s.Fingerprint)
	m["itemclass"] = []byte(s.Itemclass)
	m["login"] = []byte(s.Login)
	m["pass"] = []byte(s.Pass)
	m["url"] = []byte(s.URL)
	m["importance"] = []byte(s.Importance)
	m["comment"] = []byte(s.Comment)
	m["expirydate"] = []byte(s.Expirydate)
	m["tags"] = []byte(s.Tags)
	m["author"] = []byte(s.Author)
	m["category"] = []byte(s.Category)
	m["icon"] = []byte(s.Icon)
	m["secondeye"] = []byte(s.Secondeye)
	m["secondpass"] = []byte(s.Secondpass)
	m["template"] = []byte(s.Template)
	m["acm"] = []byte(s.Acm)
	m["paramstr"] = []byte(s.Paramstr)
	m["loginid"] = []byte(s.Loginid)
	m["passid"] = []byte(s.Passid)
	m["donotaddon"] = []byte(s.Donotaddon)
	m["markassafe"] = []byte(s.Markassafe)
	m["safemode"] = []byte(s.Safemode)

	return m
}
