package httpclient

import (
	"context"
	"sync"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
)

var (
	_ adapter.HTTPClientManager = (*Manager)(nil)
	_ adapter.LifecycleService  = (*Manager)(nil)
)

type Manager struct {
	ctx                      context.Context
	logger                   log.ContextLogger
	access                   sync.Mutex
	defines                  map[string]option.HTTPClient
	sharedTransports         map[string]*sharedManagedTransport
	managedTransports        []*ManagedTransport
	defaultTag               string
	defaultTransport         *sharedManagedTransport
	defaultTransportFallback func() (*ManagedTransport, error)
}

type sharedManagedTransport struct {
	managed *ManagedTransport
	shared  *sharedState
}

func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
	defines := make(map[string]option.HTTPClient, len(clients))
	for _, client := range clients {
		defines[client.Tag] = client
	}
	defaultTag := defaultHTTPClient
	if defaultTag == "" && len(clients) > 0 {
		defaultTag = clients[0].Tag
	}
	return &Manager{
		ctx:              ctx,
		logger:           logger,
		defines:          defines,
		sharedTransports: make(map[string]*sharedManagedTransport),
		defaultTag:       defaultTag,
	}
}

func (m *Manager) Initialize(defaultTransportFallback func() (*ManagedTransport, error)) {
	m.defaultTransportFallback = defaultTransportFallback
}

func (m *Manager) Name() string {
	return "http-client"
}

func (m *Manager) Start(stage adapter.StartStage) error {
	if stage != adapter.StartStateStart {
		return nil
	}
	if m.defaultTag != "" {
		sharedTransport, err := m.resolveShared(m.defaultTag)
		if err != nil {
			return E.Cause(err, "resolve default http client")
		}
		m.defaultTransport = sharedTransport
	}
	return nil
}

func (m *Manager) DefaultTransport() adapter.HTTPTransport {
	m.access.Lock()
	defer m.access.Unlock()
	if m.defaultTransport == nil && m.defaultTransportFallback != nil {
		transport, err := m.defaultTransportFallback()
		if err != nil {
			m.logger.Error(E.Cause(err, "create default http client"))
			return nil
		}
		m.managedTransports = append(m.managedTransports, transport)
		m.defaultTransport = &sharedManagedTransport{
			managed: transport,
			shared:  &sharedState{},
		}
	}
	if m.defaultTransport == nil {
		return nil
	}
	return newSharedRef(m.defaultTransport.managed, m.defaultTransport.shared)
}

func (m *Manager) ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
	if options.Tag != "" {
		if options.ResolveOnDetour {
			define, loaded := m.defines[options.Tag]
			if !loaded {
				return nil, E.New("http_client not found: ", options.Tag)
			}
			resolvedOptions := define.Options()
			resolvedOptions.ResolveOnDetour = true
			transport, err := NewTransport(ctx, logger, options.Tag, resolvedOptions)
			if err != nil {
				return nil, err
			}
			m.trackTransport(transport)
			return transport, nil
		}
		sharedTransport, err := m.resolveShared(options.Tag)
		if err != nil {
			return nil, err
		}
		return newSharedRef(sharedTransport.managed, sharedTransport.shared), nil
	}
	transport, err := NewTransport(ctx, logger, "", options)
	if err != nil {
		return nil, err
	}
	m.trackTransport(transport)
	return transport, nil
}

func (m *Manager) trackTransport(transport *ManagedTransport) {
	m.access.Lock()
	defer m.access.Unlock()
	m.managedTransports = append(m.managedTransports, transport)
}

func (m *Manager) resolveShared(tag string) (*sharedManagedTransport, error) {
	m.access.Lock()
	defer m.access.Unlock()
	if sharedTransport, loaded := m.sharedTransports[tag]; loaded {
		return sharedTransport, nil
	}
	define, loaded := m.defines[tag]
	if !loaded {
		return nil, E.New("http_client not found: ", tag)
	}
	transport, err := NewTransport(m.ctx, m.logger, tag, define.Options())
	if err != nil {
		return nil, E.Cause(err, "create shared http_client[", tag, "]")
	}
	sharedTransport := &sharedManagedTransport{
		managed: transport,
		shared:  &sharedState{},
	}
	m.sharedTransports[tag] = sharedTransport
	m.managedTransports = append(m.managedTransports, transport)
	return sharedTransport, nil
}

func (m *Manager) ResetNetwork() {
	m.access.Lock()
	defer m.access.Unlock()
	for _, transport := range m.managedTransports {
		transport.Reset()
	}
}

func (m *Manager) Close() error {
	m.access.Lock()
	defer m.access.Unlock()
	if m.managedTransports == nil {
		return nil
	}
	var err error
	for _, transport := range m.managedTransports {
		err = E.Append(err, transport.close(), func(err error) error {
			return E.Cause(err, "close http client")
		})
	}
	m.managedTransports = nil
	m.sharedTransports = nil
	return err
}
