package frostfs

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strconv"
	"time"

	objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
)

const (
	accessBoxCRDTNameAttr = "S3-Access-Box-CRDT-Name"
)

// AuthmateFrostFS is a mediator which implements authmate.FrostFS through pool.Pool.
type AuthmateFrostFS struct {
	frostFS *FrostFS
}

// NewAuthmateFrostFS creates new AuthmateFrostFS using provided pool.Pool.
func NewAuthmateFrostFS(p *pool.Pool) *AuthmateFrostFS {
	return &AuthmateFrostFS{frostFS: NewFrostFS(p)}
}

// ContainerExists implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) ContainerExists(ctx context.Context, idCnr cid.ID) error {
	_, err := x.frostFS.Container(ctx, idCnr)
	if err != nil {
		return fmt.Errorf("get container via connection pool: %w", err)
	}

	return nil
}

// TimeToEpoch implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) {
	return x.frostFS.TimeToEpoch(ctx, time.Now(), futureTime)
}

// CreateContainer implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) CreateContainer(ctx context.Context, prm authmate.PrmContainerCreate) (cid.ID, error) {
	basicACL := acl.Private
	// allow reading objects to OTHERS in order to provide read access to S3 gateways
	basicACL.AllowOp(acl.OpObjectGet, acl.RoleOthers)
	basicACL.AllowOp(acl.OpObjectHead, acl.RoleOthers)
	basicACL.AllowOp(acl.OpObjectSearch, acl.RoleOthers)

	return x.frostFS.CreateContainer(ctx, layer.PrmContainerCreate{
		Creator:  prm.Owner,
		Policy:   prm.Policy,
		Name:     prm.FriendlyName,
		BasicACL: basicACL,
	})
}

// GetCredsPayload implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) GetCredsPayload(ctx context.Context, addr oid.Address) ([]byte, error) {
	versions, err := x.getCredVersions(ctx, addr)
	if err != nil {
		return nil, err
	}

	credObjID := addr.Object()
	if last := versions.GetLast(); last != nil {
		credObjID = last.OjbID
	}

	res, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
		Container:   addr.Container(),
		Object:      credObjID,
		WithPayload: true,
	})
	if err != nil {
		return nil, err
	}

	defer res.Payload.Close()

	return io.ReadAll(res.Payload)
}

// CreateObject implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObjectCreate) (oid.ID, error) {
	attributes := [][2]string{{objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)}}

	if prm.NewVersionFor != nil {
		var addr oid.Address
		addr.SetContainer(prm.Container)
		addr.SetObject(*prm.NewVersionFor)

		versions, err := x.getCredVersions(ctx, addr)
		if err != nil {
			return oid.ID{}, err
		}

		if versions.GetLast() == nil {
			versions.AppendVersion(&crdt.ObjectVersion{OjbID: addr.Object()})
		}

		for key, val := range versions.GetCRDTHeaders() {
			attributes = append(attributes, [2]string{key, val})
		}

		attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()})
	}

	return x.frostFS.CreateObject(ctx, layer.PrmObjectCreate{
		Creator:    prm.Creator,
		Container:  prm.Container,
		Filepath:   prm.Filepath,
		Attributes: attributes,
		Payload:    bytes.NewReader(prm.Payload),
	})
}

func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, addr oid.Address) (*crdt.ObjectVersions, error) {
	objCredSystemName := credVersionSysName(addr.Container(), addr.Object())
	credVersions, err := x.frostFS.SearchObjects(ctx, layer.PrmObjectSearch{
		Container:      addr.Container(),
		ExactAttribute: [2]string{accessBoxCRDTNameAttr, objCredSystemName},
	})
	if err != nil {
		return nil, fmt.Errorf("search s3 access boxes: %w", err)
	}

	versions := crdt.NewObjectVersions(objCredSystemName)

	for _, id := range credVersions {
		objVersion, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
			Container:  addr.Container(),
			Object:     id,
			WithHeader: true,
		})
		if err != nil {
			return nil, fmt.Errorf("head crdt access box '%s': %w", id.EncodeToString(), err)
		}

		versions.AppendVersion(crdt.NewObjectVersion(objVersion.Head))
	}

	return versions, nil
}

func credVersionSysName(cnrID cid.ID, objID oid.ID) string {
	return cnrID.EncodeToString() + "0" + objID.EncodeToString()
}