package geodata

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/metacubex/mihomo/component/geodata"
	C "github.com/metacubex/mihomo/constant"
)

const DefaultBase = "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest"

type Config struct {
	CountryMmdbURL string
	GeoIPURL       string
	GeoSiteURL     string
}

type FileSpec struct {
	Name string
	URL  func(Config) string
}

var fileSpecs = []FileSpec{
	{Name: "Country.mmdb", URL: func(c Config) string { return c.CountryMmdbURL }},
	{Name: "geoip.dat", URL: func(c Config) string { return c.GeoIPURL }},
	{Name: "geosite.dat", URL: func(c Config) string { return c.GeoSiteURL }},
}

func DefaultConfig() Config {
	return Config{
		CountryMmdbURL: DefaultBase + "/Country.mmdb",
		GeoIPURL:       DefaultBase + "/geoip.dat",
		GeoSiteURL:     DefaultBase + "/geosite.dat",
	}
}

func ConfigFromSettings(settings map[string]string) Config {
	def := DefaultConfig()
	return Config{
		CountryMmdbURL: pickURL(settings["geo_country_mmdb_url"], def.CountryMmdbURL),
		GeoIPURL:       pickURL(settings["geo_geoip_url"], def.GeoIPURL),
		GeoSiteURL:     pickURL(settings["geo_geosite_url"], def.GeoSiteURL),
	}
}

func pickURL(value, fallback string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return fallback
	}
	return value
}

func InitHomeDir(dataDir string) string {
	geoDir := filepath.Join(dataDir, "geo")
	_ = os.MkdirAll(geoDir, 0o755)
	C.SetHomeDir(geoDir)
	return geoDir
}

func ApplyMihomoURLs(cfg Config) {
	geodata.SetMmdbUrl(cfg.CountryMmdbURL)
	geodata.SetGeoIpUrl(cfg.GeoIPURL)
	geodata.SetGeoSiteUrl(cfg.GeoSiteURL)
	geodata.SetASNUrl(DefaultBase + "/GeoLite2-ASN.mmdb")
}

type FileStatus struct {
	Name   string `json:"name"`
	Exists bool   `json:"exists"`
	Size   int64  `json:"size"`
	URL    string `json:"url"`
}

func Status(dataDir string, cfg Config) []FileStatus {
	geoDir := filepath.Join(dataDir, "geo")
	out := make([]FileStatus, 0, len(fileSpecs))
	for _, spec := range fileSpecs {
		dest := filepath.Join(geoDir, spec.Name)
		item := FileStatus{Name: spec.Name, URL: spec.URL(cfg)}
		if st, err := os.Stat(dest); err == nil && st.Size() > 0 {
			item.Exists = true
			item.Size = st.Size()
		}
		out = append(out, item)
	}
	return out
}

func Ready(dataDir string) bool {
	geoDir := filepath.Join(dataDir, "geo")
	for _, spec := range fileSpecs {
		st, err := os.Stat(filepath.Join(geoDir, spec.Name))
		if err != nil || st.Size() == 0 {
			return false
		}
	}
	return true
}

// Ensure downloads missing geo files. When force is true, all files are re-downloaded.
func Ensure(dataDir string, cfg Config, force bool) ([]string, error) {
	geoDir := InitHomeDir(dataDir)
	client := &http.Client{Timeout: 120 * time.Second}
	var updated []string
	for _, spec := range fileSpecs {
		dest := filepath.Join(geoDir, spec.Name)
		if !force {
			if st, err := os.Stat(dest); err == nil && st.Size() > 0 {
				continue
			}
		}
		url := spec.URL(cfg)
		if err := downloadFile(client, url, dest); err != nil {
			return updated, fmt.Errorf("download %s: %w (place files manually in %s)", spec.Name, err, geoDir)
		}
		updated = append(updated, spec.Name)
	}
	return updated, nil
}

func downloadFile(client *http.Client, url, dest string) error {
	resp, err := client.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	tmp := dest + ".tmp"
	f, err := os.Create(tmp)
	if err != nil {
		return err
	}
	if _, err := io.Copy(f, resp.Body); err != nil {
		f.Close()
		os.Remove(tmp)
		return err
	}
	f.Close()
	return os.Rename(tmp, dest)
}
