package hosts

import (
	"bufio"
	"context"
	"io"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/go-gost/core/hosts"
	"github.com/go-gost/core/logger"
	"github.com/go-gost/x/internal/loader"
	xlogger "github.com/go-gost/x/logger"
)

// Mapping is a static mapping from a hostname to an IP address.
type Mapping struct {
	Hostname string
	IP       net.IP
}

type options struct {
	mappings    []Mapping
	fileLoader  loader.Loader
	redisLoader loader.Loader
	httpLoader  loader.Loader
	period      time.Duration
	logger      logger.Logger
}

// Option configures a hostMapper.
type Option func(opts *options)

// MappingsOption sets the static host-to-IP mappings.
func MappingsOption(mappings []Mapping) Option {
	return func(opts *options) {
		opts.mappings = mappings
	}
}

// ReloadPeriodOption sets the period for automatic reload from external loaders.
func ReloadPeriodOption(period time.Duration) Option {
	return func(opts *options) {
		opts.period = period
	}
}

// FileLoaderOption sets a file-based loader for hosts mappings (supports both Loader and Lister).
func FileLoaderOption(fileLoader loader.Loader) Option {
	return func(opts *options) {
		opts.fileLoader = fileLoader
	}
}

// RedisLoaderOption sets a Redis-based loader for hot-reloadable hosts mappings.
func RedisLoaderOption(redisLoader loader.Loader) Option {
	return func(opts *options) {
		opts.redisLoader = redisLoader
	}
}

// HTTPLoaderOption sets an HTTP-based loader for hot-reloadable hosts mappings.
func HTTPLoaderOption(httpLoader loader.Loader) Option {
	return func(opts *options) {
		opts.httpLoader = httpLoader
	}
}

// LoggerOption sets the logger for the hostMapper.
func LoggerOption(logger logger.Logger) Option {
	return func(opts *options) {
		opts.logger = logger
	}
}

// hostMapper is a static table lookup for hostnames.
// For each host a single line should be present with the following information:
// IP_address canonical_hostname [aliases...]
// Fields of the entry are separated by any number of blanks and/or tab characters.
// Text from a "#" character until the end of the line is a comment, and is ignored.
type hostMapper struct {
	mappings   map[string][]net.IP
	options    options
	logger     logger.Logger
	mu         sync.RWMutex
	cancelFunc context.CancelFunc
}

// NewHostMapper creates a new HostMapper with the given options.
// It starts a background goroutine for periodic reload from external loaders.
func NewHostMapper(opts ...Option) hosts.HostMapper {
	var options options
	for _, opt := range opts {
		opt(&options)
	}

	ctx, cancel := context.WithCancel(context.TODO())
	p := &hostMapper{
		mappings:   make(map[string][]net.IP),
		cancelFunc: cancel,
		options:    options,
		logger:     options.logger,
	}
	if p.logger == nil {
		p.logger = xlogger.Nop()
	}

	go p.periodReload(ctx)

	return p
}

// Lookup resolves a hostname to IP addresses using the host table.
// It first tries an exact match, then a wildcard match (by prefixing with "."),
// then progressively shorter suffix matches for subdomain wildcards.
// The network parameter filters results: "ip4" returns only IPv4, "ip6" only IPv6.
func (h *hostMapper) Lookup(ctx context.Context, network, host string, opts ...hosts.Option) (ips []net.IP, ok bool) {
	h.options.logger.Debugf("lookup %s/%s", host, network)
	ips = h.lookup(host)
	if ips == nil {
		ips = h.lookup("." + host)
	}
	if ips == nil {
		s := host
		for {
			if index := strings.IndexByte(s, '.'); index > 0 {
				ips = h.lookup(s[index:])
				s = s[index+1:]
				if ips == nil {
					continue
				}
			}
			break
		}
	}

	if ips == nil {
		ips = h.lookup(".")
		if ips == nil {
			return
		}
	}

	switch network {
	case "ip4":
		var v []net.IP
		for _, ip := range ips {
			if ip = ip.To4(); ip != nil {
				v = append(v, ip)
			}
		}
		ips = v
	case "ip6":
		var v []net.IP
		for _, ip := range ips {
			if ip.To4() == nil {
				v = append(v, ip)
			}
		}
		ips = v
	default:
	}

	if len(ips) > 0 {
		h.options.logger.Debugf("host mapper: %s/%s -> %s", host, network, ips)
		ok = true
	}

	return
}

func (h *hostMapper) lookup(host string) []net.IP {
	if len(h.mappings) == 0 {
		return nil
	}

	h.mu.RLock()
	defer h.mu.RUnlock()

	return h.mappings[host]
}

func (h *hostMapper) periodReload(ctx context.Context) error {
	if err := h.reload(ctx); err != nil {
		h.logger.Warnf("reload: %v", err)
	}

	period := h.options.period
	if period <= 0 {
		return nil
	}
	if period < time.Second {
		period = time.Second
	}

	ticker := time.NewTicker(period)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			if err := h.reload(ctx); err != nil {
				h.logger.Warnf("reload: %v", err)
				// return err
			}
			h.logger.Debug("hosts reload done")
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func (h *hostMapper) reload(ctx context.Context) (err error) {
	mappings := make(map[string][]net.IP)

	mapf := func(hostname string, ip net.IP) {
		ips := mappings[hostname]
		found := false
		for i := range ips {
			if ip.Equal(ips[i]) {
				found = true
				break
			}
		}
		if !found {
			ips = append(ips, ip)
		}
		mappings[hostname] = ips
	}

	for _, mapping := range h.options.mappings {
		mapf(mapping.Hostname, mapping.IP)
	}

	m, err := h.load(ctx)
	for i := range m {
		mapf(m[i].Hostname, m[i].IP)
	}

	h.logger.Debugf("load items %d", len(mappings))

	h.mu.Lock()
	defer h.mu.Unlock()

	h.mappings = mappings

	return
}

// load fetches mappings from all configured loaders (file, redis, http).
func (h *hostMapper) load(ctx context.Context) (mappings []Mapping, err error) {
	if h.options.fileLoader != nil {
		if lister, ok := h.options.fileLoader.(loader.Lister); ok {
			list, er := lister.List(ctx)
			if er != nil {
				h.logger.Warnf("file loader: %v", er)
			}
			for _, s := range list {
				mappings = append(mappings, h.parseLine(s)...)
			}
		} else {
			r, er := h.options.fileLoader.Load(ctx)
			if er != nil {
				h.logger.Warnf("file loader: %v", er)
			}
			mappings, _ = h.parseMapping(r)
		}
	}

	if h.options.redisLoader != nil {
		if lister, ok := h.options.redisLoader.(loader.Lister); ok {
			list, er := lister.List(ctx)
			if er != nil {
				h.logger.Warnf("redis loader: %v", er)
			}
			for _, s := range list {
				mappings = append(mappings, h.parseLine(s)...)
			}
		} else {
			r, er := h.options.redisLoader.Load(ctx)
			if er != nil {
				h.logger.Warnf("redis loader: %v", er)
			}
			if m, _ := h.parseMapping(r); m != nil {
				mappings = append(mappings, m...)
			}
		}
	}
	if h.options.httpLoader != nil {
		r, er := h.options.httpLoader.Load(ctx)
		if er != nil {
			h.logger.Warnf("http loader: %v", er)
		}
		if m, _ := h.parseMapping(r); m != nil {
			mappings = append(mappings, m...)
		}
	}

	return
}

func (h *hostMapper) parseMapping(r io.Reader) (mappings []Mapping, err error) {
	if r == nil {
		return
	}

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		mappings = append(mappings, h.parseLine(scanner.Text())...)
	}
	err = scanner.Err()
	return
}

// parseLine parses a single line in hosts-file format: "IP hostname [hostname...]".
// Lines starting with "#" are comments. Invalid IP addresses or lines with no hostname are silently dropped.
func (h *hostMapper) parseLine(s string) (mappings []Mapping) {
	line := strings.Replace(s, "\t", " ", -1)
	line = strings.TrimSpace(line)
	if n := strings.IndexByte(line, '#'); n >= 0 {
		line = line[:n]
	}
	var sp []string
	for _, s := range strings.Split(line, " ") {
		if s = strings.TrimSpace(s); s != "" {
			sp = append(sp, s)
		}
	}
	if len(sp) < 2 {
		return // invalid lines are ignored
	}

	ip := net.ParseIP(sp[0])
	if ip == nil {
		return // invalid IP addresses are ignored
	}

	for _, v := range sp[1:] {
		mappings = append(mappings, Mapping{
			Hostname: v,
			IP:       ip,
		})
	}
	return
}

// Close stops the background reload goroutine and closes all loaders.
func (h *hostMapper) Close() error {
	h.cancelFunc()
	if h.options.fileLoader != nil {
		h.options.fileLoader.Close()
	}
	if h.options.redisLoader != nil {
		h.options.redisLoader.Close()
	}
	if h.options.httpLoader != nil {
		h.options.httpLoader.Close()
	}
	return nil
}
