package cluster

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/psviderski/uncloud/internal/dns"
	"github.com/psviderski/uncloud/internal/machine/api/pb"
	"github.com/psviderski/uncloud/internal/machine/store"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
const uncloudDNSKey = "uncloud_dns"

type uncloudDNSDomain struct {
	// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
	Endpoint string
	Name     string
	// TODO: encrypt the token in the store.
	Token string
}

func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainRequest) (*pb.Domain, error) {
	if err := c.checkInitialised(ctx); err != nil {
		return nil, err
	}

	if req.Endpoint == "" {
		return nil, status.Error(codes.InvalidArgument, "API endpoint not set")
	}

	if _, err := c.storedDomain(ctx); err == nil {
		return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
	} else {
		if s := status.Convert(err); s.Code() != codes.NotFound {
			return nil, err
		}
	}

	dnsClient := dns.NewClient()
	name, token, err := dnsClient.ReserveDomain(req.Endpoint)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	domain := uncloudDNSDomain{
		Endpoint: req.Endpoint,
		Name:     name,
		Token:    token,
	}
	domainJSON, err := json.Marshal(domain)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal reserved domain for store: %v", err)
	}
	if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
		return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
	}

	return &pb.Domain{Name: name}, nil
}

func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
	if err := c.checkInitialised(ctx); err != nil {
		return nil, err
	}

	domain, err := c.storedDomain(ctx)
	if err != nil {
		return nil, err
	}

	return &pb.Domain{Name: domain.Name}, nil
}

func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
	var domain uncloudDNSDomain
	var domainJSON []byte

	if err := c.store.Get(ctx, uncloudDNSKey, &domainJSON); err != nil {
		if errors.Is(err, store.ErrKeyNotFound) {
			return domain, status.Errorf(codes.NotFound, "domain not found")
		}
		return domain, status.Errorf(codes.Internal, "get domain from store: %v", err)
	}

	if err := json.Unmarshal(domainJSON, &domain); err != nil {
		return domain, status.Errorf(codes.Internal, "unmarshal domain: %v", err)
	}

	return domain, nil
}

func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
	if err := c.checkInitialised(ctx); err != nil {
		return nil, err
	}

	domain, err := c.storedDomain(ctx)
	if err != nil {
		return nil, err
	}

	if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
		return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
	}
	// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.

	return &pb.Domain{Name: domain.Name}, nil
}

func (c *Cluster) CreateDomainRecords(
	ctx context.Context, req *pb.CreateDomainRecordsRequest,
) (*pb.CreateDomainRecordsResponse, error) {
	if err := c.checkInitialised(ctx); err != nil {
		return nil, err
	}

	domain, err := c.storedDomain(ctx)
	if err != nil {
		return nil, err
	}

	dnsClient := dns.NewClient()
	recordsReq := make([]dns.RecordRequest, len(req.Records))
	for i, r := range req.Records {
		recordsReq[i] = dns.RecordRequest{
			Name:   r.Name,
			Type:   dns.RecordType(r.Type.String()),
			Values: r.Values,
		}
	}

	recordsResp, err := dnsClient.CreateRecords(domain.Endpoint, domain.Name, domain.Token, recordsReq)
	if err != nil {
		return nil, err
	}

	resp := &pb.CreateDomainRecordsResponse{
		Records: make([]*pb.DNSRecord, len(recordsResp)),
	}
	for i, r := range recordsResp {
		resp.Records[i] = &pb.DNSRecord{
			Name:   r.FQDN,
			Values: r.Values,
		}

		switch r.Type {
		case dns.RecordTypeA:
			resp.Records[i].Type = pb.DNSRecord_A
		case dns.RecordTypeAAAA:
			resp.Records[i].Type = pb.DNSRecord_AAAA
		}
	}

	return resp, nil
}
