// Copyright 2022-2025 The sacloud/iaas-api-go Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fake

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"reflect"
	"sort"
	"strings"
	"sync"

	"github.com/fatih/structs"
	"github.com/mitchellh/go-homedir"
	"github.com/sacloud/iaas-api-go"
	"github.com/sacloud/iaas-api-go/types"
)

const defaultJSONFilePath = "libsacloud-fake-store.json"

// JSONFileStore .
type JSONFileStore struct {
	Path       string
	Ctx        context.Context
	NoInitData bool

	mu    sync.Mutex
	cache JSONFileStoreData
}

// JSONFileStoreData .
type JSONFileStoreData map[string]map[string]interface{}

// MarshalJSON .
func (d JSONFileStoreData) MarshalJSON() ([]byte, error) {
	var transformed []map[string]interface{}
	for cacheKey, resources := range d {
		resourceKey, zone := d.parseKey(cacheKey)
		for id, value := range resources {
			var mapValue map[string]interface{}
			if d.isArrayOrSlice(value) {
				mapValue = map[string]interface{}{
					"Values": value,
				}
			} else {
				mapValue = structs.Map(value)
			}

			mapValue["ID"] = id
			mapValue["ZoneName"] = zone
			mapValue["ResourceType"] = resourceKey

			transformed = append(transformed, mapValue)
		}
	}

	sort.Slice(transformed, func(i, j int) bool {
		rt1 := transformed[i]["ResourceType"].(string)
		rt2 := transformed[j]["ResourceType"].(string)
		if rt1 == rt2 {
			id1 := types.StringID(transformed[i]["ID"].(string))
			id2 := types.StringID(transformed[j]["ID"].(string))
			return id1 < id2
		}
		return rt1 < rt2
	})

	return json.MarshalIndent(transformed, "", "\t")
}

// UnmarshalJSON .
func (d *JSONFileStoreData) UnmarshalJSON(data []byte) error {
	var transformed []map[string]interface{}
	if err := json.Unmarshal(data, &transformed); err != nil {
		return err
	}

	dest := JSONFileStoreData{}
	for _, mapValue := range transformed {
		rawID, ok := mapValue["ID"]
		if !ok {
			return fmt.Errorf("invalid JSON: 'ID' field is missing: %v", mapValue)
		}
		id := rawID.(string)

		rawZone, ok := mapValue["ZoneName"]
		if !ok {
			return fmt.Errorf("invalid JSON: 'ZoneName' field is missing: %v", mapValue)
		}
		zone := rawZone.(string)

		rawRt, ok := mapValue["ResourceType"]
		if !ok {
			return fmt.Errorf("invalid JSON: 'ResourceType' field is missing: %v", mapValue)
		}
		rt := rawRt.(string)

		var resources map[string]interface{}
		r, ok := dest[d.key(rt, zone)]
		if ok {
			resources = r
		} else {
			resources = map[string]interface{}{}
		}
		if v, ok := mapValue["Values"]; ok {
			resources[id] = v
		} else {
			resources[id] = mapValue
		}

		dest[d.key(rt, zone)] = resources
	}

	*d = dest
	return nil
}

func (d *JSONFileStoreData) isArrayOrSlice(v interface{}) bool {
	rt := reflect.TypeOf(v)
	switch rt.Kind() {
	case reflect.Slice, reflect.Array:
		return true
	case reflect.Ptr:
		return d.isArrayOrSlice(reflect.ValueOf(v).Elem().Interface())
	}
	return false
}

func (d *JSONFileStoreData) key(resourceKey, zone string) string {
	return fmt.Sprintf("%s/%s", resourceKey, zone)
}

func (d *JSONFileStoreData) parseKey(k string) (string, string) {
	ss := strings.Split(k, "/")
	if len(ss) == 2 {
		return ss[0], ss[1]
	}
	return "", ""
}

// NewJSONFileStore .
func NewJSONFileStore(path string) *JSONFileStore {
	return &JSONFileStore{
		Path:  path,
		cache: make(map[string]map[string]interface{}),
	}
}

// Init .
func (s *JSONFileStore) Init() error {
	if s.Ctx == nil {
		s.Ctx = context.Background()
	}
	if s.Path == "" {
		s.Path = defaultJSONFilePath
	}

	// expand filepath
	path, err := homedir.Expand(s.Path)
	if err != nil {
		return err
	}
	s.Path = path

	if stat, err := os.Stat(s.Path); err == nil {
		if stat.IsDir() {
			return fmt.Errorf("path %q is directory", s.Path)
		}
	} else {
		if _, err := os.Create(s.Path); err != nil {
			return err
		}
	}

	if err := s.load(); err != nil {
		return err
	}
	s.startWatcher()
	return nil
}

// NeedInitData .
func (s *JSONFileStore) NeedInitData() bool {
	if s.NoInitData {
		return false
	}
	return len(s.cache) < 2
}

// Put .
func (s *JSONFileStore) Put(resourceKey, zone string, id types.ID, value interface{}) {
	s.mu.Lock()
	defer s.mu.Unlock()

	values := s.values(resourceKey, zone)
	if values == nil {
		values = map[string]interface{}{}
	}
	values[id.String()] = value
	s.cache[s.key(resourceKey, zone)] = values

	s.store() //nolint
}

// Get .
func (s *JSONFileStore) Get(resourceKey, zone string, id types.ID) interface{} {
	s.mu.Lock()
	defer s.mu.Unlock()

	values := s.values(resourceKey, zone)
	if values == nil {
		return nil
	}
	return values[id.String()]
}

// List .
func (s *JSONFileStore) List(resourceKey, zone string) []interface{} {
	s.mu.Lock()
	defer s.mu.Unlock()

	values := s.values(resourceKey, zone)
	var ret []interface{}
	for _, v := range values {
		ret = append(ret, v)
	}
	return ret
}

// Delete .
func (s *JSONFileStore) Delete(resourceKey, zone string, id types.ID) {
	s.mu.Lock()
	defer s.mu.Unlock()

	values := s.values(resourceKey, zone)
	if values != nil {
		delete(values, id.String())
	}
	s.store() //nolint
}

var jsonResourceTypeMap = map[string]func() interface{}{
	ResourceArchive:           func() interface{} { return &iaas.Archive{} },
	ResourceAuthStatus:        func() interface{} { return &iaas.AuthStatus{} },
	ResourceAutoBackup:        func() interface{} { return &iaas.AutoBackup{} },
	ResourceBill:              func() interface{} { return &iaas.Bill{} },
	ResourceBridge:            func() interface{} { return &iaas.Bridge{} },
	ResourceCDROM:             func() interface{} { return &iaas.CDROM{} },
	ResourceContainerRegistry: func() interface{} { return &iaas.ContainerRegistry{} },
	ResourceCoupon:            func() interface{} { return &iaas.Coupon{} },
	ResourceDatabase:          func() interface{} { return &iaas.Database{} },
	ResourceDisk:              func() interface{} { return &iaas.Disk{} },
	ResourceDiskPlan:          func() interface{} { return &iaas.DiskPlan{} },
	ResourceDNS:               func() interface{} { return &iaas.DNS{} },
	ResourceEnhancedDB:        func() interface{} { return &iaas.EnhancedDB{} },
	ResourceESME:              func() interface{} { return &iaas.ESME{} },
	ResourceGSLB:              func() interface{} { return &iaas.GSLB{} },
	ResourceIcon:              func() interface{} { return &iaas.Icon{} },
	ResourceInterface:         func() interface{} { return &iaas.Interface{} },
	ResourceInternet:          func() interface{} { return &iaas.Internet{} },
	ResourceInternetPlan:      func() interface{} { return &iaas.InternetPlan{} },
	ResourceIPAddress:         func() interface{} { return &iaas.IPAddress{} },
	ResourceIPv6Net:           func() interface{} { return &iaas.IPv6Net{} },
	ResourceIPv6Addr:          func() interface{} { return &iaas.IPv6Addr{} },
	ResourceLicense:           func() interface{} { return &iaas.License{} },
	ResourceLicenseInfo:       func() interface{} { return &iaas.LicenseInfo{} },
	ResourceLoadBalancer:      func() interface{} { return &iaas.LoadBalancer{} },
	ResourceLocalRouter:       func() interface{} { return &iaas.LocalRouter{} },
	ResourceMobileGateway:     func() interface{} { return &iaas.MobileGateway{} },
	ResourceNFS:               func() interface{} { return &iaas.NFS{} },
	ResourceNote:              func() interface{} { return &iaas.Note{} },
	ResourcePacketFilter:      func() interface{} { return &iaas.PacketFilter{} },
	ResourcePrivateHost:       func() interface{} { return &iaas.PrivateHost{} },
	ResourcePrivateHostPlan:   func() interface{} { return &iaas.PrivateHostPlan{} },
	ResourceProxyLB:           func() interface{} { return &iaas.ProxyLB{} },
	ResourceRegion:            func() interface{} { return &iaas.Region{} },
	ResourceServer:            func() interface{} { return &iaas.Server{} },
	ResourceServerPlan:        func() interface{} { return &iaas.ServerPlan{} },
	ResourceServiceClass:      func() interface{} { return &iaas.ServiceClass{} },
	ResourceSIM:               func() interface{} { return &iaas.SIM{} },
	ResourceSimpleMonitor:     func() interface{} { return &iaas.SimpleMonitor{} },
	ResourceSubnet:            func() interface{} { return &iaas.Subnet{} },
	ResourceSSHKey:            func() interface{} { return &iaas.SSHKey{} },
	ResourceSwitch:            func() interface{} { return &iaas.Switch{} },
	ResourceVPCRouter:         func() interface{} { return &iaas.VPCRouter{} },
	ResourceZone:              func() interface{} { return &iaas.Zone{} },

	valuePoolResourceKey:         func() interface{} { return &valuePool{} },
	"BillDetails":                func() interface{} { return &[]*iaas.BillDetail{} },
	"ContainerRegistryUsers":     func() interface{} { return &[]*iaas.ContainerRegistryUser{} },
	"DatabaseParameter":          func() interface{} { return map[string]interface{}{} },
	"ESMELogs":                   func() interface{} { return &[]*iaas.ESMELogs{} },
	"LocalRouterStatus":          func() interface{} { return &iaas.LocalRouterHealth{} },
	"MobileGatewayDNS":           func() interface{} { return &iaas.MobileGatewayDNSSetting{} },
	"MobileGatewaySIMRoutes":     func() interface{} { return &[]*iaas.MobileGatewaySIMRoute{} },
	"MobileGatewaySIMs":          func() interface{} { return &[]*iaas.MobileGatewaySIMInfo{} },
	"MobileGatewayTrafficConfig": func() interface{} { return &iaas.MobileGatewayTrafficControl{} },
	"ProxyLBStatus":              func() interface{} { return &iaas.ProxyLBHealth{} },
	"SIMNetworkOperator":         func() interface{} { return &[]*iaas.SIMNetworkOperatorConfig{} },
}

func (s *JSONFileStore) unmarshalResource(resourceKey string, data []byte) (interface{}, error) {
	f, ok := jsonResourceTypeMap[resourceKey]
	if !ok {
		panic(fmt.Errorf("type %q is not registered", resourceKey))
	}
	v := f()
	if err := json.Unmarshal(data, v); err != nil {
		return nil, err
	}
	return v, nil
}

func (s *JSONFileStore) store() error {
	data, err := json.MarshalIndent(s.cache, "", "\t")
	if err != nil {
		return err
	}
	return os.WriteFile(s.Path, data, 0600)
}

func (s *JSONFileStore) load() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	data, err := os.ReadFile(s.Path)
	if err != nil {
		return err
	}
	if len(data) == 0 {
		return nil
	}

	var cache = JSONFileStoreData{}
	if err := json.Unmarshal(data, &cache); err != nil {
		return err
	}

	var loaded = make(map[string]map[string]interface{})
	for cacheKey, values := range cache {
		resourceKey, _ := s.parseKey(cacheKey)

		var dest = make(map[string]interface{})
		for id, v := range values {
			data, err := json.Marshal(v)
			if err != nil {
				return err
			}
			cv, err := s.unmarshalResource(resourceKey, data)
			if err != nil {
				return err
			}
			dest[id] = cv
		}
		loaded[cacheKey] = dest
	}
	s.cache = loaded
	return nil
}

func (s *JSONFileStore) key(resourceKey, zone string) string {
	return fmt.Sprintf("%s/%s", resourceKey, zone)
}

func (s *JSONFileStore) parseKey(k string) (string, string) {
	ss := strings.Split(k, "/")
	if len(ss) == 2 {
		return ss[0], ss[1]
	}
	return "", ""
}

func (s *JSONFileStore) values(resourceKey, zone string) map[string]interface{} {
	return s.cache[s.key(resourceKey, zone)]
}
