package store

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"
)

const SecretMask = "********"

type Device struct {
	ID                 string
	Name               string
	DeviceType         string
	Interface          string
	ControlDevice      string
	ATPort             string
	USBPath            string
	AudioDevice        string
	ModemIMEI          string
	SIMPIN             string
	APN                string
	ProxyPort          int
	BaudRate           int
	DataBits           int
	StopBits           int
	Parity             string
	DeviceBackend      string
	ESIMTransport      string
	QMIUseProxy        bool
	QMIProxyPath       string
	QMIProxyExecutable string
	NetworkEnabled     bool
	SMSEnabled         bool
	VoWiFiEnabled      bool
	Extra              json.RawMessage
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

type DeviceRuntime struct {
	DeviceID          string
	Running           bool
	Healthy           bool
	ControlOnline     bool
	PhysicalPresent   bool
	WorkerRunning     bool
	DataConnected     bool
	RadioRegistered   bool
	NetworkConnected  bool
	FlightMode        bool
	LifecyclePhase    string
	LifecycleReason   string
	PublicIP          string
	PrivateIP         string
	Operator          string
	NativeMCC         string
	NativeMNC         string
	NativeSPN         string
	NetworkMode       string
	NetworkDuplex     string
	RadioBand         string
	RadioChannel      int
	SignalDBM         int
	SignalRSRP        *int
	SignalRSRQ        *int
	SignalSINR        *int
	IMEI              string
	ICCID             string
	IMSI              string
	Firmware          string
	RegStatus         int
	RegStatusText     string
	PSAttached        *bool
	SIMInserted       *bool
	OperatingMode     *int
	PhoneNumber       string
	PhoneNumberSource string
	Traffic           json.RawMessage
	Extra             json.RawMessage
	UpdatedAt         time.Time
}

type VoWiFiRuntime struct {
	DeviceID          string
	Phase             string
	DataplaneMode     string
	ICCID             string
	IMSI              string
	SIMReady          bool
	AccessReady       bool
	TunnelReady       bool
	IMSReady          bool
	SMSReady          bool
	RegStatus         int
	RegStatusText     string
	NetworkMode       string
	LocalPhone        string
	PhoneNumberSource string
	LastErrorClass    string
	LastError         string
	LastReason        string
	Tunnel            json.RawMessage
	IMSCore           json.RawMessage
	SMSIP             json.RawMessage
	Extra             json.RawMessage
	UpdatedAt         time.Time
}

// PhoneAssociation is a number explicitly published by IMS for one SIM. It is
// keyed by ICCID so a verified number survives service restarts and follows the
// SIM without ever being inferred from IMSI.
type PhoneAssociation struct {
	ICCID     string
	DeviceID  string
	Number    string
	Source    string
	CreatedAt time.Time
	UpdatedAt time.Time
}

type AutomaticTask struct {
	ID           int64           `json:"id"`
	Name         string          `json:"name"`
	Enabled      bool            `json:"enabled"`
	DeviceID     string          `json:"device_id"`
	ProfileICCID string          `json:"profile_iccid"`
	ProfileAID   string          `json:"profile_aid"`
	TaskType     string          `json:"task_type"`
	Environment  string          `json:"environment"`
	IntervalDays int             `json:"interval_days"`
	StartDate    string          `json:"start_date"`
	RunTime      string          `json:"run_time"`
	Timezone     string          `json:"timezone"`
	Payload      json.RawMessage `json:"payload"`
	RetryCount   int             `json:"retry_count"`
	Notify       bool            `json:"notify"`
	NextRunAt    time.Time       `json:"next_run_at"`
	LastRunAt    time.Time       `json:"last_run_at,omitempty"`
	LastStatus   string          `json:"last_status"`
	LastError    string          `json:"last_error"`
	CreatedAt    time.Time       `json:"created_at"`
	UpdatedAt    time.Time       `json:"updated_at"`
}

type AutomaticTaskRun struct {
	ID          int64     `json:"id"`
	TaskID      int64     `json:"task_id"`
	DeviceID    string    `json:"device_id"`
	ScheduledAt time.Time `json:"scheduled_at"`
	StartedAt   time.Time `json:"started_at,omitempty"`
	FinishedAt  time.Time `json:"finished_at,omitempty"`
	Status      string    `json:"status"`
	Attempts    int       `json:"attempts"`
	Output      string    `json:"output"`
	Error       string    `json:"error"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

type SMSMessage struct {
	ID            int64
	MessageID     string
	DeviceID      string
	ModemIMEI     string
	IMSI          string
	Peer          string
	Direction     string
	Body          string
	Timestamp     time.Time
	Status        string
	Source        string
	PartsTotal    int
	DeliveryState string
	Read          bool
	Extra         json.RawMessage
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

type SMSFilter struct {
	DeviceID  string
	ModemIMEI string
	IMSI      string
	Peer      string
	Since     time.Time
	Until     time.Time
	BeforeID  int64
	Limit     int
}

// SMSDeliveryReport is network evidence for one submitted SMS part. The
// message reference is the TP-MR returned in SMS-STATUS-REPORT.
type SMSDeliveryReport struct {
	DeviceID          string
	ModemIMEI         string
	IMSI              string
	Peer              string
	Source            string
	MessageReference  int
	StatusCode        int
	DeliveryState     string
	ServiceCenterTime *time.Time
	DischargeTime     *time.Time
	ReceivedAt        time.Time
}

type SMSContact struct {
	DeviceID      string
	DeviceName    string
	ModemIMEI     string
	IMSI          string
	LocalPhone    string
	Peer          string
	DisplayName   string
	LastMessage   string
	LastTimestamp time.Time
	Direction     string
	LastSMSID     int64
	UnreadCount   int
	MessageCount  int
}

type LocalProxyConfig struct {
	ID          string
	Name        string
	Mode        string
	DeviceID    string
	ListenAddr  string
	ListenPort  int
	Enabled     bool
	AuthEnabled bool
	Username    string
	Password    string
	Extra       json.RawMessage
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

func (value LocalProxyConfig) Redacted() LocalProxyConfig {
	if value.Password != "" {
		value.Password = SecretMask
	}
	return value
}

func (value LocalProxyConfig) Public() LocalProxyConfig {
	value.Password = ""
	return value
}

func (value LocalProxyConfig) SensitiveValues() []string {
	if value.Password == "" || value.Password == SecretMask {
		return nil
	}
	return []string{value.Password}
}

type UpstreamProxy struct {
	ID        string
	Name      string
	Addr      string
	Username  string
	Password  string
	Enabled   bool
	Extra     json.RawMessage
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (value UpstreamProxy) Redacted() UpstreamProxy {
	if value.Password != "" {
		value.Password = SecretMask
	}
	return value
}

func (value UpstreamProxy) Public() UpstreamProxy {
	value.Password = ""
	return value
}

func (value UpstreamProxy) SensitiveValues() []string {
	if value.Password == "" || value.Password == SecretMask {
		return nil
	}
	return []string{value.Password}
}

type CountryRule struct {
	CountryCode     string
	CountryName     string
	UpstreamProxyID string
	Enabled         bool
	Extra           json.RawMessage
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

// DeviceProxyBinding selects the SOCKS5 upstream for exactly one eSIM profile.
// ICCID is globally unique, while one proxy may serve profiles on many devices.
type DeviceProxyBinding struct {
	DeviceID        string
	ICCID           string
	ProfileName     string
	UpstreamProxyID string
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

type NotificationSetting struct {
	Channel         string
	Enabled         bool
	Config          json.RawMessage
	SensitiveFields []string
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

func (value NotificationSetting) Redacted() NotificationSetting {
	value.Config = redactJSONFields(value.Config, value.SensitiveFields, SecretMask)
	return value
}

func (value NotificationSetting) Public() NotificationSetting {
	value.Config = redactJSONFields(value.Config, value.SensitiveFields, "")
	return value
}

func (value NotificationSetting) SensitiveValues() []string {
	document, err := decodeJSONObject(value.Config)
	if err != nil {
		return nil
	}
	values := make([]string, 0, len(value.SensitiveFields))
	for _, field := range value.SensitiveFields {
		collectJSONStringValues(getJSONPath(document, field), &values)
	}
	return uniqueNonemptyStrings(values)
}

type AppSetting struct {
	Key       string
	Value     json.RawMessage
	Sensitive bool
	UpdatedAt time.Time
}

func (value AppSetting) Redacted() AppSetting {
	if value.Sensitive {
		value.Value = json.RawMessage(strconv.Quote(SecretMask))
	}
	return value
}

func (value AppSetting) Public() AppSetting {
	if value.Sensitive {
		value.Value = json.RawMessage(`null`)
	}
	return value
}

func (value AppSetting) SensitiveValues() []string {
	if !value.Sensitive {
		return nil
	}
	var decoded any
	decoder := json.NewDecoder(bytes.NewReader(value.Value))
	decoder.UseNumber()
	if decoder.Decode(&decoded) != nil {
		return nil
	}
	var values []string
	collectJSONStringValues(decoded, &values)
	return uniqueNonemptyStrings(values)
}

type SensitiveValuesProvider interface {
	SensitiveValues() []string
}

func RedactText(text string, providers ...SensitiveValuesProvider) string {
	var secrets []string
	for _, provider := range providers {
		if !nilSensitiveProvider(provider) {
			secrets = append(secrets, provider.SensitiveValues()...)
		}
	}
	sort.Slice(secrets, func(i, j int) bool {
		return len(secrets[i]) > len(secrets[j])
	})
	for _, secret := range secrets {
		if secret != "" && secret != SecretMask {
			text = strings.ReplaceAll(text, secret, SecretMask)
		}
	}
	return text
}

func nilSensitiveProvider(provider SensitiveValuesProvider) bool {
	if provider == nil {
		return true
	}
	value := reflect.ValueOf(provider)
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
		reflect.Pointer, reflect.Slice:
		return value.IsNil()
	default:
		return false
	}
}

func collectJSONStringValues(value any, result *[]string) {
	switch typed := value.(type) {
	case string:
		if typed != "" && typed != SecretMask {
			*result = append(*result, typed)
		}
	case []any:
		for _, item := range typed {
			collectJSONStringValues(item, result)
		}
	case map[string]any:
		for _, item := range typed {
			collectJSONStringValues(item, result)
		}
	}
}

type AuditEvent struct {
	ID         int64
	Actor      string
	Action     string
	EntityType string
	EntityID   string
	Outcome    string
	RemoteAddr string
	Details    json.RawMessage
	CreatedAt  time.Time
}

type AuditFilter struct {
	Actor      string
	Action     string
	EntityType string
	EntityID   string
	Since      time.Time
	Until      time.Time
	BeforeID   int64
	Limit      int
}

type LogEvent struct {
	ID      int64
	Time    time.Time
	Level   string
	Message string
	Caller  string
	Fields  json.RawMessage
}

type LogFilter struct {
	Level    string
	Since    time.Time
	Until    time.Time
	BeforeID int64
	Limit    int
}

type CardPolicy struct {
	ICCID             string
	NetworkEnabled    bool
	VoWiFiEnabled     bool
	AirplaneEnabled   bool
	APN               string
	IPVersion         string
	CustomPhoneNumber string
	Source            string
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

type CardAPNProfile struct {
	ID               int64
	ICCID            string
	APN              string
	Username         string
	Password         string
	Proxy            string
	MCC              string
	MNC              string
	IPVersion        string
	RoamingIPVersion string
	AuthType         string
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

type TrafficBucket struct {
	DeviceID    string
	Bucket      string
	PeriodStart time.Time
	RXBytes     int64
	TXBytes     int64
}

func (value TrafficBucket) TotalBytes() int64 {
	return value.RXBytes + value.TXBytes
}

type TrafficFilter struct {
	DeviceID string
	Bucket   string
	Since    time.Time
	Until    time.Time
	Limit    int
}

func normalizeJSONObject(value json.RawMessage) (json.RawMessage, error) {
	if len(bytes.TrimSpace(value)) == 0 {
		return json.RawMessage(`{}`), nil
	}
	document, err := decodeJSONObject(value)
	if err != nil {
		return nil, err
	}
	normalized, err := json.Marshal(document)
	if err != nil {
		return nil, err
	}
	return normalized, nil
}

func normalizeJSONValue(value json.RawMessage) (json.RawMessage, error) {
	if len(bytes.TrimSpace(value)) == 0 {
		return json.RawMessage(`null`), nil
	}
	if !json.Valid(value) {
		return nil, errors.New("invalid JSON value")
	}
	return append(json.RawMessage(nil), value...), nil
}

func decodeJSONObject(value json.RawMessage) (map[string]any, error) {
	var document map[string]any
	decoder := json.NewDecoder(bytes.NewReader(value))
	decoder.UseNumber()
	if err := decoder.Decode(&document); err != nil {
		return nil, err
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, errors.New("JSON value must contain exactly one object")
		}
		return nil, err
	}
	if document == nil {
		return nil, errors.New("JSON value must be an object")
	}
	return document, nil
}

func redactJSONFields(value json.RawMessage, fields []string, replacement string) json.RawMessage {
	document, err := decodeJSONObject(value)
	if err != nil {
		return json.RawMessage(`{}`)
	}
	for _, field := range fields {
		if current := getJSONPath(document, field); current != nil {
			setJSONPath(document, field, redactJSONValue(current, replacement))
		}
	}
	encoded, err := json.Marshal(document)
	if err != nil {
		return json.RawMessage(`{}`)
	}
	return encoded
}

func mergeJSONSecrets(
	incoming json.RawMessage,
	existing json.RawMessage,
	fields []string,
) (json.RawMessage, error) {
	next, err := decodeJSONObject(incoming)
	if err != nil {
		return nil, err
	}
	current, err := decodeJSONObject(existing)
	if err != nil {
		current = map[string]any{}
	}
	for _, field := range fields {
		value := getJSONPath(next, field)
		if previous := getJSONPath(current, field); previous != nil {
			setJSONPath(next, field, mergeJSONSecretValue(value, previous))
		}
	}
	return json.Marshal(next)
}

func redactJSONValue(value any, replacement string) any {
	switch typed := value.(type) {
	case string:
		return replacement
	case []any:
		result := make([]any, len(typed))
		for index, item := range typed {
			result[index] = redactJSONValue(item, replacement)
		}
		return result
	default:
		return replacement
	}
}

func mergeJSONSecretValue(incoming, existing any) any {
	if incoming == nil {
		return existing
	}
	switch next := incoming.(type) {
	case string:
		if next == "" || next == SecretMask {
			return existing
		}
	case []any:
		previous, ok := existing.([]any)
		if !ok {
			return incoming
		}
		merged := make([]any, len(next))
		for index, value := range next {
			if index < len(previous) {
				merged[index] = mergeJSONSecretValue(value, previous[index])
			} else {
				merged[index] = value
			}
		}
		return merged
	}
	return incoming
}

func getJSONPath(document map[string]any, path string) any {
	if strings.TrimSpace(path) == "" {
		return nil
	}
	parts := strings.Split(path, ".")
	var current any = document
	for _, part := range parts {
		object, ok := current.(map[string]any)
		if !ok {
			return nil
		}
		current, ok = object[part]
		if !ok {
			return nil
		}
	}
	return current
}

func setJSONPath(document map[string]any, path string, value any) {
	if strings.TrimSpace(path) == "" {
		return
	}
	parts := strings.Split(path, ".")
	current := document
	for _, part := range parts[:len(parts)-1] {
		next, ok := current[part].(map[string]any)
		if !ok {
			next = map[string]any{}
			current[part] = next
		}
		current = next
	}
	current[parts[len(parts)-1]] = value
}

func boolInt(value bool) int {
	if value {
		return 1
	}
	return 0
}

func nullableBool(value *bool) any {
	if value == nil {
		return nil
	}
	return boolInt(*value)
}

func nullableInt(value *int) any {
	if value == nil {
		return nil
	}
	return *value
}
