package bindcodestore

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

// BindCode 公司绑定码（Z13）：兑换后挂默认同步落点。
type BindCode struct {
	Code         string     `json:"code"`
	TenantID     int64      `json:"tenant_id"`
	ChannelID    string     `json:"channel_id,omitempty"`
	OnlineDBID   string     `json:"online_db_id,omitempty"`
	DatabaseName string     `json:"database_name,omitempty"`
	MaxUses      int        `json:"max_uses"`
	UsedCount    int        `json:"used_count"`
	Revoked      bool       `json:"revoked"`
	ExpiresAt    *time.Time `json:"expires_at,omitempty"`
	CreatedBy    int64      `json:"created_by"`
	CreatedAt    time.Time  `json:"created_at"`
	Note         string     `json:"note,omitempty"`
}

type CreateInput struct {
	ChannelID    string
	OnlineDBID   string
	DatabaseName string
	MaxUses      int           // 默认 1
	ExpiresIn    time.Duration // 0=7天
	Note         string
}

type Store interface {
	Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*BindCode, error)
	List(ctx context.Context, tenantID int64) ([]BindCode, error)
	Revoke(ctx context.Context, tenantID int64, code string) error
	Redeem(ctx context.Context, code string) (*BindCode, error) // 校验并 +1 used
	Get(ctx context.Context, code string) (*BindCode, error)
}

type FileStore struct {
	mu   sync.Mutex
	path string
}

func NewFileStore(dir string) (*FileStore, error) {
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, err
	}
	return &FileStore{path: filepath.Join(dir, "bind_codes.json")}, nil
}

func (s *FileStore) read() ([]BindCode, error) {
	b, err := os.ReadFile(s.path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	if len(b) == 0 {
		return nil, nil
	}
	var list []BindCode
	if err := json.Unmarshal(b, &list); err != nil {
		return nil, err
	}
	return list, nil
}

func (s *FileStore) write(list []BindCode) error {
	b, err := json.MarshalIndent(list, "", "  ")
	if err != nil {
		return err
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, b, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, s.path)
}

func genCode() (string, error) {
	var buf [8]byte
	if _, err := rand.Read(buf[:]); err != nil {
		return "", err
	}
	return strings.ToUpper(hex.EncodeToString(buf[:])), nil
}

func (s *FileStore) Create(_ context.Context, tenantID, createdBy int64, in CreateInput) (*BindCode, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	list, err := s.read()
	if err != nil {
		return nil, err
	}
	code, err := genCode()
	if err != nil {
		return nil, err
	}
	maxUses := in.MaxUses
	if maxUses <= 0 {
		maxUses = 1
	}
	expIn := in.ExpiresIn
	if expIn <= 0 {
		expIn = 7 * 24 * time.Hour
	}
	exp := time.Now().UTC().Add(expIn)
	bc := BindCode{
		Code:         code,
		TenantID:     tenantID,
		ChannelID:    strings.TrimSpace(in.ChannelID),
		OnlineDBID:   strings.TrimSpace(in.OnlineDBID),
		DatabaseName: strings.TrimSpace(in.DatabaseName),
		MaxUses:      maxUses,
		CreatedBy:    createdBy,
		CreatedAt:    time.Now().UTC(),
		ExpiresAt:    &exp,
		Note:         in.Note,
	}
	list = append(list, bc)
	if err := s.write(list); err != nil {
		return nil, err
	}
	return &bc, nil
}

func (s *FileStore) List(_ context.Context, tenantID int64) ([]BindCode, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	list, err := s.read()
	if err != nil {
		return nil, err
	}
	out := make([]BindCode, 0)
	for _, bc := range list {
		if bc.TenantID == tenantID {
			out = append(out, bc)
		}
	}
	return out, nil
}

func (s *FileStore) Revoke(_ context.Context, tenantID int64, code string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	code = strings.ToUpper(strings.TrimSpace(code))
	list, err := s.read()
	if err != nil {
		return err
	}
	for i := range list {
		if list[i].Code == code && list[i].TenantID == tenantID {
			list[i].Revoked = true
			return s.write(list)
		}
	}
	return fmt.Errorf("bind code not found")
}

func (s *FileStore) Get(_ context.Context, code string) (*BindCode, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	code = strings.ToUpper(strings.TrimSpace(code))
	list, err := s.read()
	if err != nil {
		return nil, err
	}
	for i := range list {
		if list[i].Code == code {
			cp := list[i]
			return &cp, nil
		}
	}
	return nil, fmt.Errorf("bind code not found")
}

func (s *FileStore) Redeem(_ context.Context, code string) (*BindCode, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	code = strings.ToUpper(strings.TrimSpace(code))
	list, err := s.read()
	if err != nil {
		return nil, err
	}
	for i := range list {
		bc := &list[i]
		if bc.Code != code {
			continue
		}
		if bc.Revoked {
			return nil, fmt.Errorf("bind code revoked")
		}
		if bc.ExpiresAt != nil && time.Now().UTC().After(*bc.ExpiresAt) {
			return nil, fmt.Errorf("bind code expired")
		}
		if bc.UsedCount >= bc.MaxUses {
			return nil, fmt.Errorf("bind code exhausted")
		}
		bc.UsedCount++
		if err := s.write(list); err != nil {
			return nil, err
		}
		cp := *bc
		return &cp, nil
	}
	return nil, fmt.Errorf("bind code not found")
}
