package policy

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/nspcc-dev/neofs-sdk-go/netmap"
)

type (
	filter struct {
		Name    string   `json:"name,omitempty"`
		Key     string   `json:"key,omitempty"`
		Op      string   `json:"op,omitempty"`
		Value   string   `json:"value,omitempty"`
		Filters []filter `json:"filters,omitempty"`
	}
	replica struct {
		Count    uint32 `json:"count"`
		Selector string `json:"selector,omitempty"`
	}
	selector struct {
		Count     uint32 `json:"count"`
		Attribute string `json:"attribute"`
		Filter    string `json:"filter,omitempty"`
		Name      string `json:"name,omitempty"`
		Clause    string `json:"clause,omitempty"`
	}
	placement struct {
		Replicas  []replica  `json:"replicas"`
		CBF       uint32     `json:"container_backup_factor,omitempty"`
		Selectors []selector `json:"selectors,omitempty"`
		Filters   []filter   `json:"filters,omitempty"`
	}
)

// ToJSON converts placement policy to JSON.
func ToJSON(np *netmap.PlacementPolicy) ([]byte, error) {
	p := new(placement)
	p.CBF = np.ContainerBackupFactor()
	p.Filters = make([]filter, len(np.Filters()))
	for i, f := range np.Filters() {
		p.Filters[i].fromNetmap(&f)
	}
	p.Selectors = make([]selector, len(np.Selectors()))
	for i, s := range np.Selectors() {
		p.Selectors[i].fromNetmap(&s)
	}
	p.Replicas = make([]replica, len(np.Replicas()))
	for i, r := range np.Replicas() {
		p.Replicas[i].fromNetmap(&r)
	}
	return json.Marshal(p)
}

// FromJSON creates placement policy from JSON.
func FromJSON(data []byte) (*netmap.PlacementPolicy, error) {
	p := new(placement)
	if err := json.Unmarshal(data, p); err != nil {
		return nil, err
	}

	rs := make([]netmap.Replica, len(p.Replicas))
	for i := range p.Replicas {
		rs[i] = *p.Replicas[i].toNetmap()
	}

	var fs []netmap.Filter
	if len(p.Filters) != 0 {
		fs = make([]netmap.Filter, len(p.Filters))
		for i := range p.Filters {
			f, err := p.Filters[i].toNetmap()
			if err != nil {
				return nil, err
			}
			fs[i] = *f
		}
	}

	var ss []netmap.Selector
	if len(p.Selectors) != 0 {
		ss = make([]netmap.Selector, len(p.Selectors))
		for i := range p.Selectors {
			s, err := p.Selectors[i].toNetmap()
			if err != nil {
				return nil, err
			}
			ss[i] = *s
		}
	}

	pp := new(netmap.PlacementPolicy)
	pp.SetReplicas(rs...)
	pp.SetContainerBackupFactor(p.CBF)
	pp.SetFilters(fs...)
	pp.SetSelectors(ss...)
	return pp, nil
}

func (r *replica) toNetmap() *netmap.Replica {
	nr := new(netmap.Replica)
	nr.SetCount(r.Count)
	nr.SetSelector(r.Selector)
	return nr
}

func (r *replica) fromNetmap(nr *netmap.Replica) {
	r.Count = nr.Count()
	r.Selector = nr.Selector()
}

func (f *filter) toNetmap() (*netmap.Filter, error) {
	var op netmap.Operation
	switch strings.ToUpper(f.Op) {
	case "EQ":
		op = netmap.OpEQ
	case "NE":
		op = netmap.OpNE
	case "GT":
		op = netmap.OpGT
	case "GE":
		op = netmap.OpGE
	case "LT":
		op = netmap.OpLT
	case "LE":
		op = netmap.OpLE
	case "AND":
		op = netmap.OpAND
	case "OR":
		op = netmap.OpOR
	case "":
	default:
		return nil, fmt.Errorf("%w: '%s'", ErrUnknownOp, f.Op)
	}

	var fs []netmap.Filter
	if len(f.Filters) != 0 {
		fs = make([]netmap.Filter, len(f.Filters))
		for i := range f.Filters {
			var err error
			fsp, err := f.Filters[i].toNetmap()
			if err != nil {
				return nil, err
			}
			fs[i] = *fsp
		}
	}

	nf := new(netmap.Filter)
	nf.SetInnerFilters(fs...)
	nf.SetOperation(op)
	nf.SetName(f.Name)
	nf.SetValue(f.Value)
	nf.SetKey(f.Key)
	return nf, nil
}

func (f *filter) fromNetmap(nf *netmap.Filter) {
	f.Name = nf.Name()
	f.Key = nf.Key()
	f.Value = nf.Value()
	switch nf.Operation() {
	case netmap.OpEQ:
		f.Op = "EQ"
	case netmap.OpNE:
		f.Op = "NE"
	case netmap.OpGT:
		f.Op = "GT"
	case netmap.OpGE:
		f.Op = "GE"
	case netmap.OpLT:
		f.Op = "LT"
	case netmap.OpLE:
		f.Op = "LE"
	case netmap.OpAND:
		f.Op = "AND"
	case netmap.OpOR:
		f.Op = "OR"
	default:
		// do nothing
	}
	if nf.InnerFilters() != nil {
		f.Filters = make([]filter, len(nf.InnerFilters()))
		for i, sf := range nf.InnerFilters() {
			f.Filters[i].fromNetmap(&sf)
		}
	}
}

func (s *selector) toNetmap() (*netmap.Selector, error) {
	var c netmap.Clause
	switch strings.ToUpper(s.Clause) {
	case "SAME":
		c = netmap.ClauseSame
	case "DISTINCT":
		c = netmap.ClauseDistinct
	case "":
	default:
		return nil, fmt.Errorf("%w: '%s'", ErrUnknownClause, s.Clause)
	}
	ns := new(netmap.Selector)
	ns.SetName(s.Name)
	ns.SetAttribute(s.Attribute)
	ns.SetCount(s.Count)
	ns.SetClause(c)
	ns.SetFilter(s.Filter)
	return ns, nil
}

func (s *selector) fromNetmap(ns *netmap.Selector) {
	s.Name = ns.Name()
	s.Filter = ns.Filter()
	s.Count = ns.Count()
	s.Attribute = ns.Attribute()
	switch ns.Clause() {
	case netmap.ClauseSame:
		s.Clause = "same"
	case netmap.ClauseDistinct:
		s.Clause = "distinct"
	default:
		// do nothing
	}
	s.Name = ns.Name()
}