package provider

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/omegion/ssh-manager/internal"
)

const (
	// OnePasswordDefaultPrefix default prefix for OnePasswordItem.
	//nolint:gosec // not security issue.
	OnePasswordDefaultPrefix = "SSHKeys__"
	// OnePasswordCommand base command for OnePassword.
	OnePasswordCommand = "op"
)

// OnePassword for connection.
type OnePassword struct {
	Commander internal.Commander
}

// OnePasswordItemOverview is item adapter for provider OnePasswordItem.
type OnePasswordItemOverview struct {
	Title *string `json:"title"`
}

// OnePasswordItemDetails is item adapter for provider Item.
type OnePasswordItemDetails struct {
	NotesPlain *string `json:"notesPlain"`
}

// OnePasswordItem is item adapter for provider OnePasswordItem.
type OnePasswordItem struct {
	UUID     *string                 `json:"uuid"`
	Details  OnePasswordItemDetails  `json:"details"`
	Overview OnePasswordItemOverview `json:"overview"`
}

// GetName returns name of the provider.
func (o OnePassword) GetName() string {
	return OnePasswordCommand
}

// Add adds given item to OnePassword.
func (o OnePassword) Add(item *Item) error {
	_, err := o.Get(GetOptions{
		Name: item.Name,
	})
	if err == nil {
		return ItemAlreadyExistsError{Name: item.Name}
	}

	encodedValues, err := item.EncodeValues()
	if err != nil {
		return err
	}

	command := o.Commander.Executor.CommandContext(
		context.Background(),
		OnePasswordCommand,
		"create",
		"item",
		"login",
		fmt.Sprintf("notesPlain=%s", encodedValues),
		"--title",
		fmt.Sprintf("%s%s", OnePasswordDefaultPrefix, item.Name),
		"--tags",
		strings.Replace(OnePasswordDefaultPrefix, "__", "", 1),
	)

	var stderr bytes.Buffer

	command.SetStderr(&stderr)

	_, err = command.Output()
	if err != nil {
		return ExecutionFailedError{
			Command: "op create",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	return nil
}

// Get gets Item from OnePassword with given item name.
func (o OnePassword) Get(options GetOptions) (*Item, error) {
	command := o.Commander.Executor.CommandContext(
		context.Background(),
		OnePasswordCommand,
		"get",
		"item",
		fmt.Sprintf("%s%s", OnePasswordDefaultPrefix, options.Name),
	)

	var stderr bytes.Buffer

	command.SetStderr(&stderr)

	log.Debugln(fmt.Sprintf("Getting item %s in OnePassword.", options.Name))

	output, err := command.Output()
	if err != nil {
		return &Item{}, ExecutionFailedError{
			Command: "op get item",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	var opItem OnePasswordItem

	err = json.Unmarshal(output, &opItem)
	if err != nil {
		return &Item{}, ExecutionFailedError{
			Command: "op get item",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	item := Item{
		ID:   *opItem.UUID,
		Name: strings.Replace(*opItem.Overview.Title, OnePasswordDefaultPrefix, "", 1),
	}

	log.Debugln(fmt.Sprintf("Decoding keys for item %s.", options.Name))

	decodedRawNotes, err := base64.StdEncoding.DecodeString(*opItem.Details.NotesPlain)
	if err != nil {
		return &Item{}, ExecutionFailedError{
			Command: "op get item",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	err = json.Unmarshal(decodedRawNotes, &item.Values)
	if err != nil {
		return &Item{}, ExecutionFailedError{
			Command: "op get item",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	return &item, nil
}

// List lists all added SSH keys.
func (o OnePassword) List(_ ListOptions) ([]*Item, error) {
	command := o.Commander.Executor.CommandContext(
		context.Background(),
		OnePasswordCommand,
		"list",
		"items",
		"--categories",
		"login",
		"--tags",
		strings.Replace(OnePasswordDefaultPrefix, "__", "", 1),
	)

	var stderr bytes.Buffer

	command.SetStderr(&stderr)

	log.Debugln("Getting items in OnePassword.")

	output, err := command.Output()
	if err != nil {
		return []*Item{}, ExecutionFailedError{
			Command: "op list",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	var opItems []OnePasswordItem

	err = json.Unmarshal(output, &opItems)
	if err != nil {
		return []*Item{}, ExecutionFailedError{
			Command: "op list",
			Message: fmt.Sprintf("%v: %s", err, stderr.String()),
		}
	}

	items := make([]*Item, 0)

	for _, item := range opItems {
		items = append(items, &Item{
			ID:   *item.UUID,
			Name: strings.Replace(*item.Overview.Title, OnePasswordDefaultPrefix, "", 1),
		})
	}

	return items, nil
}
