package model

import (
	"fmt"
	"sort"
	"strings"

	"github.com/jinzhu/copier"
	"go.uber.org/zap"
	"k8s.io/utils/ptr"

	"github.com/bakito/adguardhome-sync/internal/utils"
)

// Clone the config.
func (c *DhcpStatus) Clone() *DhcpStatus {
	clone := &DhcpStatus{}
	_ = copier.Copy(clone, c)
	return clone
}

func (c *DhcpStatus) cleanV4V6() {
	if c.V4 != nil && !c.V4.isValid() {
		c.V4 = nil
	}
	if c.V6 != nil && !c.V6.isValid() {
		c.V6 = nil
	}
}

// CleanAndEquals dhcp server config equal check where V4 and V6 are cleaned in advance.
func (c *DhcpStatus) CleanAndEquals(o *DhcpStatus) bool {
	c.cleanV4V6()
	o.cleanV4V6()
	return c.Equals(o)
}

// Equals dhcp server config equal check.
func (c *DhcpStatus) Equals(o *DhcpStatus) bool {
	return utils.JSONEquals(c, o)
}

func (c *DhcpStatus) HasConfig() bool {
	return (c.V4 != nil && c.V4.isValid()) || (c.V6 != nil && c.V6.isValid())
}

func (j DhcpConfigV4) isValid() bool {
	return j.GatewayIp != nil && *j.GatewayIp != "" &&
		j.SubnetMask != nil && *j.SubnetMask != "" &&
		j.RangeStart != nil && *j.RangeStart != "" &&
		j.RangeEnd != nil && *j.RangeEnd != ""
}

func (j DhcpConfigV6) isValid() bool {
	return j.RangeStart != nil && *j.RangeStart != ""
}

type DhcpStaticLeases []DhcpStaticLease

// MergeDhcpStaticLeases the leases.
func MergeDhcpStaticLeases(l, other *[]DhcpStaticLease) (adds, removes DhcpStaticLeases) {
	var thisLeases []DhcpStaticLease
	var otherLeases []DhcpStaticLease

	if l != nil {
		thisLeases = *l
	}
	if other != nil {
		otherLeases = *other
	}
	current := make(map[string]DhcpStaticLease)

	for _, le := range thisLeases {
		current[le.Mac] = le
	}

	for _, le := range otherLeases {
		if _, ok := current[le.Mac]; ok {
			delete(current, le.Mac)
		} else {
			adds = append(adds, le)
		}
	}

	for _, rr := range current {
		removes = append(removes, rr)
	}

	return adds, removes
}

// Equals dns config equal check.
func (c *DNSConfig) Equals(o *DNSConfig) bool {
	cc := c.Clone()
	oo := o.Clone()
	cc.Sort()
	oo.Sort()

	return utils.JSONEquals(cc, oo)
}

func (c *DNSConfig) Clone() *DNSConfig {
	return utils.Clone(c, &DNSConfig{})
}

// Sort dns config.
func (c *DNSConfig) Sort() {
	if c.UpstreamDns != nil {
		sort.Strings(*c.UpstreamDns)
	}

	if c.UpstreamDns != nil {
		sort.Strings(*c.BootstrapDns)
	}

	if c.UpstreamDns != nil {
		sort.Strings(*c.LocalPtrUpstreams)
	}
}

// Equals access list equal check.
func (al *AccessList) Equals(o *AccessList) bool {
	return EqualsStringSlice(al.AllowedClients, o.AllowedClients, true) &&
		EqualsStringSlice(al.DisallowedClients, o.DisallowedClients, true) &&
		EqualsStringSlice(al.BlockedHosts, o.BlockedHosts, true)
}

func EqualsStringSlice(a, b *[]string, sortIt bool) bool {
	if a == nil && b == nil {
		return true
	}

	if a == nil || b == nil {
		return false
	}

	aa := *a
	bb := *b
	if sortIt {
		sort.Strings(aa)
		sort.Strings(bb)
	}
	if len(aa) != len(bb) {
		return false
	}
	for i, v := range aa {
		if v != bb[i] {
			return false
		}
	}
	return true
}

// Sort clients.
func (cl *Client) Sort() {
	if cl.Ids != nil {
		sort.Strings(*cl.Ids)
	}
	if cl.Tags != nil {
		sort.Strings(*cl.Tags)
	}
	if cl.BlockedServices != nil {
		sort.Strings(*cl.BlockedServices)
	}
	if cl.Upstreams != nil {
		sort.Strings(*cl.Upstreams)
	}
}

// PrepareDiff so we skip it in diff.
func (cl *Client) PrepareDiff() *string {
	var tz *string
	bss := cl.BlockedServicesSchedule
	if bss != nil && bss.Mon == nil && bss.Tue == nil && bss.Wed == nil &&
		bss.Thu == nil && bss.Fri == nil && bss.Sat == nil && bss.Sun == nil {
		tz = cl.BlockedServicesSchedule.TimeZone
		cl.BlockedServicesSchedule.TimeZone = nil
	}
	return tz
}

// AfterDiff reset after diff.
func (cl *Client) AfterDiff(tz *string) {
	if cl.BlockedServicesSchedule != nil {
		cl.BlockedServicesSchedule.TimeZone = tz
	}
}

// Equals Clients equal check.
func (cl *Client) Equals(o *Client) bool {
	cl.Sort()
	o.Sort()

	bssCl := cl.PrepareDiff()
	bssO := o.PrepareDiff()

	defer func() {
		cl.AfterDiff(bssCl)
		o.AfterDiff(bssO)
	}()

	return utils.JSONEquals(cl, o)
}

// Add ac client.
func (clients *Clients) Add(cl Client) {
	if clients.Clients == nil {
		clients.Clients = &ClientsArray{cl}
	} else {
		a := append(*clients.Clients, cl)
		clients.Clients = &a
	}
}

// Merge merge Clients.
func (clients *Clients) Merge(other *Clients) (adds, removes, updates []*Client) {
	current := make(map[string]*Client)
	if clients.Clients != nil {
		cc := *clients.Clients
		for _, client := range cc {
			current[*client.Name] = &client
		}
	}

	expected := make(map[string]*Client)
	if other.Clients != nil {
		oc := *other.Clients
		for _, client := range oc {
			expected[*client.Name] = &client
		}
	}

	for _, cl := range expected {
		if oc, ok := current[*cl.Name]; ok {
			if !cl.Equals(oc) {
				updates = append(updates, cl)
			}
			delete(current, *cl.Name)
		} else {
			adds = append(adds, cl)
		}
	}

	for _, rr := range current {
		removes = append(removes, rr)
	}

	return adds, updates, removes
}

// Key RewriteEntry key.
func (re *RewriteEntry) Key() string {
	var d string
	var a string
	if re.Domain != nil {
		d = *re.Domain
	}
	if re.Answer != nil {
		a = *re.Answer
	}
	return fmt.Sprintf("%s#%s", d, a)
}

// RewriteEntries list of RewriteEntry.
type RewriteEntries []RewriteEntry

// Merge RewriteEntries.
func (rwe *RewriteEntries) Merge(other *RewriteEntries) (adds, removes, duplicates RewriteEntries) {
	current := make(map[string]RewriteEntry)

	processed := make(map[string]bool)
	for _, rr := range *rwe {
		if _, ok := processed[rr.Key()]; !ok {
			current[rr.Key()] = rr
			processed[rr.Key()] = true
		} else {
			// remove duplicate
			removes = append(removes, rr)
		}
	}

	for _, rr := range *other {
		if _, ok := current[rr.Key()]; ok {
			delete(current, rr.Key())
		} else {
			if _, ok := processed[rr.Key()]; !ok {
				adds = append(adds, rr)
				processed[rr.Key()] = true
			} else {
				//	skip duplicate
				duplicates = append(duplicates, rr)
			}
		}
	}

	for _, rr := range current {
		removes = append(removes, rr)
	}

	return adds, removes, duplicates
}

func MergeFilters(this, other *[]Filter) (adds, updates, removes []Filter) {
	if this == nil && other == nil {
		return nil, nil, nil
	}

	current := make(map[string]*Filter)

	if this != nil {
		for _, fi := range *this {
			current[fi.Url] = &fi
		}
	}

	if other != nil {
		for _, rr := range *other {
			if c, ok := current[rr.Url]; ok {
				if !c.Equals(&rr) {
					updates = append(updates, rr)
				}
				delete(current, rr.Url)
			} else {
				adds = append(adds, rr)
			}
		}
	}

	for _, rr := range current {
		removes = append(removes, *rr)
	}

	return adds, updates, removes
}

// Equals Filter equal check.
func (f *Filter) Equals(o *Filter) bool {
	return f.Enabled == o.Enabled && f.Url == o.Url && f.Name == o.Name
}

type QueryLogConfigWithIgnored struct {
	QueryLogConfig

	// Ignored List of host names, which should not be written to log
	Ignored []string `json:"ignored,omitempty"`
}

// Equals QueryLogConfig equal check.
func (qlc *QueryLogConfigWithIgnored) Equals(o *QueryLogConfigWithIgnored) bool {
	return utils.JSONEquals(qlc, o)
}

// Equals QueryLogConfigInterval equal check.
func (qlc *QueryLogConfigInterval) Equals(o *QueryLogConfigInterval) bool {
	return ptrEquals(qlc, o)
}

func ptrEquals[T comparable](a, b *T) bool {
	if a == nil && b == nil {
		return true
	}
	var aa T
	if a != nil {
		aa = *a
	}
	var bb T
	if b != nil {
		bb = *b
	}

	return aa == bb
}

// EnableConfig API struct.
type EnableConfig struct {
	Enabled bool `json:"enabled"`
}

func (ssc *SafeSearchConfig) Equals(o *SafeSearchConfig) bool {
	return ptrEquals(ssc.Enabled, o.Enabled) &&
		ptrEquals(ssc.Bing, o.Bing) &&
		ptrEquals(ssc.Duckduckgo, o.Duckduckgo) &&
		ptrEquals(ssc.Google, o.Google) &&
		ptrEquals(ssc.Pixabay, o.Pixabay) &&
		ptrEquals(ssc.Yandex, o.Yandex) &&
		ptrEquals(ssc.Youtube, o.Youtube)
}

func (pi *ProfileInfo) Equals(o *ProfileInfo, withTheme bool) bool {
	return pi.Language == o.Language && (!withTheme || pi.Theme == o.Theme)
}

func (pi *ProfileInfo) ShouldSyncFor(o *ProfileInfo, withTheme bool) *ProfileInfo {
	if pi.Equals(o, withTheme) {
		return nil
	}
	merged := &ProfileInfo{Name: pi.Name, Language: pi.Language, Theme: pi.Theme}
	if o.Language != "" {
		merged.Language = o.Language
	}
	if withTheme && o.Theme != "" {
		merged.Theme = o.Theme
	}
	if merged.Name == "" || merged.Language == "" || merged.Equals(pi, false) {
		return nil
	}
	return merged
}

func (bss *BlockedServicesSchedule) Equals(o *BlockedServicesSchedule) bool {
	return utils.JSONEquals(bss, o)
}

func (bss *BlockedServicesSchedule) ServicesString() string {
	return ArrayString(bss.Ids)
}

func ArrayString(a *[]string) string {
	if a == nil {
		return "[]"
	}
	sorted := *a
	sort.Strings(sorted)
	return fmt.Sprintf("[%s]", strings.Join(sorted, ","))
}

func (c *DNSConfig) Sanitize(l *zap.SugaredLogger) {
	// disable UsePrivatePtrResolvers if not configured
	// https://github.com/AdguardTeam/AdGuardHome/issues/6820
	if c.UsePrivatePtrResolvers != nil && *c.UsePrivatePtrResolvers &&
		(c.LocalPtrUpstreams == nil || len(*c.LocalPtrUpstreams) == 0) {
		l.Warn(
			"disabling replica 'Use private reverse DNS resolvers' as no 'Private reverse DNS servers' are configured on origin",
		)
		c.UsePrivatePtrResolvers = utils.Ptr(false)
	}
}

// Equals GetStatsConfigResponse equal check.
func (sc *GetStatsConfigResponse) Equals(o *GetStatsConfigResponse) bool {
	return utils.JSONEquals(sc, o)
}

func NewStats() *Stats {
	return &Stats{
		NumBlockedFiltering:     ptr.To(0),
		NumReplacedParental:     ptr.To(0),
		NumReplacedSafesearch:   ptr.To(0),
		NumReplacedSafebrowsing: ptr.To(0),
		NumDnsQueries:           ptr.To(0),

		BlockedFiltering:     ptr.To(make([]int, 24)),
		DnsQueries:           ptr.To(make([]int, 24)),
		ReplacedParental:     ptr.To(make([]int, 24)),
		ReplacedSafebrowsing: ptr.To(make([]int, 24)),
	}
}

func (s *Stats) Add(other *Stats) {
	s.NumBlockedFiltering = addInt(s.NumBlockedFiltering, other.NumBlockedFiltering)
	s.NumReplacedSafebrowsing = addInt(s.NumReplacedSafebrowsing, other.NumReplacedSafebrowsing)
	s.NumDnsQueries = addInt(s.NumDnsQueries, other.NumDnsQueries)
	s.NumReplacedSafesearch = addInt(s.NumReplacedSafesearch, other.NumReplacedSafesearch)
	s.NumReplacedParental = addInt(s.NumReplacedParental, other.NumReplacedParental)

	s.BlockedFiltering = sumUp(s.BlockedFiltering, other.BlockedFiltering)
	s.DnsQueries = sumUp(s.DnsQueries, other.DnsQueries)
	s.ReplacedParental = sumUp(s.ReplacedParental, other.ReplacedParental)
	s.ReplacedSafebrowsing = sumUp(s.ReplacedSafebrowsing, other.ReplacedSafebrowsing)
}

func addInt(t, add *int) *int {
	if add != nil {
		return ptr.To(*t + *add)
	}
	return t
}

func sumUp(t, o *[]int) *[]int {
	if o != nil {
		tt := *t
		oo := *o
		var sum []int
		for i := range tt {
			if len(oo) >= i {
				sum = append(sum, tt[i]+oo[i])
			}
		}
		return &sum
	}
	return t
}

func (c *TlsConfig) Equals(config *TlsConfig) bool {
	return utils.JSONEquals(c, config)
}
