package service

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"gorm.io/gorm"

	"oci-portal/internal/crypto"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

// 代理配置错误;api 层映射 400 / 409。
var (
	// ErrProxyInvalid 表示字段校验失败。
	ErrProxyInvalid = errors.New("代理配置非法")
	// ErrProxyInUse 表示代理仍被租户引用,拒绝删除。
	ErrProxyInUse = errors.New("代理仍被租户关联,请先解除关联")
)

// ProxyService 管理出站代理配置;密码 AES-GCM 加密落库。
type ProxyService struct {
	db     *gorm.DB
	cipher *crypto.Cipher
	// wg 追踪创建 / 更新后的异步出口地理探测,Wait 供优雅关停与测试收敛
	wg sync.WaitGroup
	// geoClientFor 构造经代理出站的探测 client;测试注入替身避免真实外呼
	geoClientFor func(*oci.ProxySpec) *http.Client
}

// NewProxyService 组装依赖。
func NewProxyService(db *gorm.DB, cipher *crypto.Cipher) *ProxyService {
	return &ProxyService{db: db, cipher: cipher, geoClientFor: oci.HTTPClientFor}
}

// Wait 阻塞至在途的地理探测全部完成;进程退出前调用。
func (s *ProxyService) Wait() {
	s.wg.Wait()
}

// ProxyView 是代理的脱敏视图,绝不含密码明文。
type ProxyView struct {
	ID          uint   `json:"id"`
	Name        string `json:"name"`
	Type        string `json:"type"`
	Host        string `json:"host"`
	Port        int    `json:"port"`
	Username    string `json:"username"`
	PasswordSet bool   `json:"passwordSet"`
	// Country / City 为出口实测地区;GeoAt 空串表示尚未探测(前端显示「检测中」)
	Country string `json:"country"`
	City    string `json:"city"`
	GeoAt   string `json:"geoAt"`
	// UsedBy 为关联此代理的租户数,前端据此提示删除约束
	UsedBy    int64  `json:"usedBy"`
	CreatedAt string `json:"createdAt"`
}

// ProxyInput 是创建 / 更新请求体;Name 缺省自动生成,
// Password 为 nil 表示沿用已存值,空串表示清除。
type ProxyInput struct {
	Name     string  `json:"name"`
	Type     string  `json:"type" binding:"required"`
	Host     string  `json:"host" binding:"required"`
	Port     int     `json:"port" binding:"required"`
	Username string  `json:"username"`
	Password *string `json:"password"`
}

// validateProxyInput 校验类型 / 端口 / 主机;错误信息用户可读。
func validateProxyInput(in ProxyInput) error {
	if in.Type != "socks5" && in.Type != "http" && in.Type != "https" {
		return fmt.Errorf("类型仅支持 socks5 / http / https: %w", ErrProxyInvalid)
	}
	if in.Port < 1 || in.Port > 65535 {
		return fmt.Errorf("端口须在 1-65535 之间: %w", ErrProxyInvalid)
	}
	if strings.TrimSpace(in.Host) == "" {
		return fmt.Errorf("主机不能为空: %w", ErrProxyInvalid)
	}
	return nil
}

// List 返回全部代理的脱敏视图,附带引用计数。
func (s *ProxyService) List(ctx context.Context) ([]ProxyView, error) {
	rows := []model.Proxy{}
	if err := s.db.WithContext(ctx).Order("id").Find(&rows).Error; err != nil {
		return nil, fmt.Errorf("list proxies: %w", err)
	}
	out := make([]ProxyView, 0, len(rows))
	for i := range rows {
		v, err := s.viewOf(ctx, &rows[i])
		if err != nil {
			return nil, err
		}
		out = append(out, v)
	}
	return out, nil
}

// viewOf 组装单条脱敏视图。
func (s *ProxyService) viewOf(ctx context.Context, p *model.Proxy) (ProxyView, error) {
	var used int64
	err := s.db.WithContext(ctx).Model(&model.OciConfig{}).Where("proxy_id = ?", p.ID).Count(&used).Error
	if err != nil {
		return ProxyView{}, fmt.Errorf("count proxy refs: %w", err)
	}
	return ProxyView{
		ID: p.ID, Name: p.Name, Type: p.Type, Host: p.Host, Port: p.Port,
		Username: p.Username, PasswordSet: p.PasswordEnc != "", UsedBy: used,
		Country: p.Country, City: p.City, GeoAt: formatTimePtr(p.GeoAt),
		CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
	}, nil
}

// formatTimePtr 把可空时间格式化为 RFC3339,nil 返回空串。
func formatTimePtr(t *time.Time) string {
	if t == nil {
		return ""
	}
	return t.Format("2006-01-02T15:04:05Z07:00")
}

// autoProxyName 名称缺省时生成 `{type}-{host}:{port}`,与现有代理重名时追加序号后缀。
func (s *ProxyService) autoProxyName(ctx context.Context, in ProxyInput, selfID uint) string {
	base := fmt.Sprintf("%s-%s:%d", in.Type, strings.TrimSpace(in.Host), in.Port)
	if len(base) > 56 {
		base = base[:56]
	}
	name := base
	for i := 2; i <= 999; i++ {
		var n int64
		s.db.WithContext(ctx).Model(&model.Proxy{}).
			Where("name = ? AND id <> ?", name, selfID).Count(&n)
		if n == 0 {
			return name
		}
		name = fmt.Sprintf("%s-%d", base, i)
	}
	return name
}

// Create 新建代理;名称唯一冲突返回可读错误,成功后异步探测出口地区。
func (s *ProxyService) Create(ctx context.Context, in ProxyInput) (ProxyView, error) {
	if err := validateProxyInput(in); err != nil {
		return ProxyView{}, err
	}
	if strings.TrimSpace(in.Name) == "" {
		in.Name = s.autoProxyName(ctx, in, 0)
	}
	row := model.Proxy{
		Name: strings.TrimSpace(in.Name), Type: in.Type,
		Host: strings.TrimSpace(in.Host), Port: in.Port, Username: in.Username,
	}
	if err := s.fillPassword(&row, in.Password); err != nil {
		return ProxyView{}, err
	}
	if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
		return ProxyView{}, fmt.Errorf("create proxy: %w", err)
	}
	s.probeGeoAsync(row.ID)
	return s.viewOf(ctx, &row)
}

// Update 更新代理;密码缺省沿用、空串清除,名称留空重新自动生成,成功后重测地区。
func (s *ProxyService) Update(ctx context.Context, id uint, in ProxyInput) (ProxyView, error) {
	if err := validateProxyInput(in); err != nil {
		return ProxyView{}, err
	}
	if strings.TrimSpace(in.Name) == "" {
		in.Name = s.autoProxyName(ctx, in, id)
	}
	var row model.Proxy
	if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
		return ProxyView{}, fmt.Errorf("find proxy %d: %w", id, err)
	}
	row.Name, row.Type = strings.TrimSpace(in.Name), in.Type
	row.Host, row.Port, row.Username = strings.TrimSpace(in.Host), in.Port, in.Username
	if err := s.fillPassword(&row, in.Password); err != nil {
		return ProxyView{}, err
	}
	if err := s.db.WithContext(ctx).Save(&row).Error; err != nil {
		return ProxyView{}, fmt.Errorf("update proxy: %w", err)
	}
	s.probeGeoAsync(row.ID)
	return s.viewOf(ctx, &row)
}

// fillPassword 按输入语义写密文:nil 沿用、空串清除、非空加密覆盖。
func (s *ProxyService) fillPassword(row *model.Proxy, password *string) error {
	if password == nil {
		return nil
	}
	if *password == "" {
		row.PasswordEnc = ""
		return nil
	}
	enc, err := s.cipher.EncryptString(*password)
	if err != nil {
		return fmt.Errorf("encrypt proxy password: %w", err)
	}
	row.PasswordEnc = enc
	return nil
}

// Delete 删除代理;仍被租户引用时拒绝。
func (s *ProxyService) Delete(ctx context.Context, id uint) error {
	var used int64
	err := s.db.WithContext(ctx).Model(&model.OciConfig{}).Where("proxy_id = ?", id).Count(&used).Error
	if err != nil {
		return fmt.Errorf("count proxy refs: %w", err)
	}
	if used > 0 {
		return ErrProxyInUse
	}
	res := s.db.WithContext(ctx).Delete(&model.Proxy{}, id)
	if res.Error != nil {
		return fmt.Errorf("delete proxy: %w", res.Error)
	}
	if res.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

// SetTenants 设置关联此代理的租户全集:列表内的租户改挂到本代理(含从
// 其他代理改挂),列表外已关联的解除;整体事务生效,返回新的关联计数。
func (s *ProxyService) SetTenants(ctx context.Context, id uint, cfgIDs []uint) (int64, error) {
	if err := s.db.WithContext(ctx).First(&model.Proxy{}, id).Error; err != nil {
		return 0, fmt.Errorf("find proxy %d: %w", id, err)
	}
	ids, err := s.validTenantIDs(ctx, cfgIDs)
	if err != nil {
		return 0, err
	}
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		unbind := tx.Model(&model.OciConfig{}).Where("proxy_id = ?", id)
		if len(ids) > 0 {
			unbind = unbind.Where("id NOT IN ?", ids)
		}
		if err := unbind.Update("proxy_id", nil).Error; err != nil {
			return fmt.Errorf("unbind tenants: %w", err)
		}
		if len(ids) == 0 {
			return nil
		}
		err := tx.Model(&model.OciConfig{}).Where("id IN ?", ids).Update("proxy_id", id).Error
		if err != nil {
			return fmt.Errorf("bind tenants: %w", err)
		}
		return nil
	})
	if err != nil {
		return 0, err
	}
	return int64(len(ids)), nil
}

// validTenantIDs 去重并校验租户 ID 均存在;含无效 ID 时报 ErrProxyInvalid。
func (s *ProxyService) validTenantIDs(ctx context.Context, cfgIDs []uint) ([]uint, error) {
	seen := map[uint]struct{}{}
	ids := make([]uint, 0, len(cfgIDs))
	for _, v := range cfgIDs {
		if _, ok := seen[v]; !ok {
			seen[v] = struct{}{}
			ids = append(ids, v)
		}
	}
	if len(ids) == 0 {
		return ids, nil
	}
	var n int64
	err := s.db.WithContext(ctx).Model(&model.OciConfig{}).Where("id IN ?", ids).Count(&n).Error
	if err != nil {
		return nil, fmt.Errorf("count tenants: %w", err)
	}
	if n != int64(len(ids)) {
		return nil, fmt.Errorf("存在无效的租户 ID: %w", ErrProxyInvalid)
	}
	return ids, nil
}

// SpecOf 解密并组装 SDK 用的代理参数;id 为 nil 返回 nil(直连)。
func (s *ProxyService) SpecOf(ctx context.Context, id *uint) (*oci.ProxySpec, error) {
	if id == nil {
		return nil, nil
	}
	var row model.Proxy
	if err := s.db.WithContext(ctx).First(&row, *id).Error; err != nil {
		return nil, fmt.Errorf("find proxy %d: %w", *id, err)
	}
	password := ""
	if row.PasswordEnc != "" {
		p, err := s.cipher.DecryptString(row.PasswordEnc)
		if err != nil {
			return nil, fmt.Errorf("decrypt proxy password: %w", err)
		}
		password = p
	}
	return &oci.ProxySpec{
		Type: row.Type, Host: row.Host, Port: row.Port,
		Username: row.Username, Password: password,
	}, nil
}
