package debian

import (
	"bufio"
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/ulikunitz/xz"
)

var packageNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9+.-]*$`)

const defaultDebianMirror = "https://deb.debian.org/debian"

var (
	defaultSuites     = []string{"stable"} // Только stable для скорости
	defaultComponents = []string{"main"}   // Только main для скорости
)

// SourceInfo describes Debian source package coordinates resolved over HTTP.
type SourceInfo struct {
	RequestedPackage string
	SourcePackage    string
	DSCURL           string
	DSCSHA256        string
	DebianVersion    string
	UpstreamVersion  string
	Suite            string
	Component        string
	BuildDepends     []string // Build dependencies from DSC file
}

const maxAutoBuildDepth = 15 // Увеличим для сложных цепочек зависимостей
const parallelWorkers = 20   // Параллельный поиск зависимостей (HTTP запросы)
const buildWorkers = 4       // Параллельное построение пакетов

// CachedSources holds parsed Sources data for fast lookups
type CachedSources struct {
	packages map[string]*sourceRecord // key: package name
	path     string                   // cache file path
	mu       sync.RWMutex
}

// Resolver queries Debian source metadata over HTTP.
type Resolver struct {
	client        *http.Client
	mirrors       []string
	suites        []string
	components    []string
	cache         map[string]*SourceInfo    // Кеш найденных пакетов
	cacheMu       sync.RWMutex              // Защита кеша
	cachedSources map[string]*CachedSources // Кешированные Sources файлы по ключу
	sourcesMu     sync.RWMutex              // Защита cachedSources map
}

// ResolverOption customizes resolver behavior.
type ResolverOption func(*Resolver)

func WithHTTPClient(client *http.Client) ResolverOption {
	return func(r *Resolver) {
		if client != nil {
			r.client = client
		}
	}
}

func WithMirrors(mirrors []string) ResolverOption {
	return func(r *Resolver) {
		r.mirrors = normalizeList(mirrors)
	}
}

func WithSuites(suites []string) ResolverOption {
	return func(r *Resolver) {
		r.suites = normalizeList(suites)
	}
}

func WithComponents(components []string) ResolverOption {
	return func(r *Resolver) {
		r.components = normalizeList(components)
	}
}

func NewResolver(opts ...ResolverOption) *Resolver {
	r := &Resolver{
		client: &http.Client{
			Timeout: 10 * time.Second, // Reduced timeout for faster failure
			Transport: &http.Transport{
				TLSHandshakeTimeout:   5 * time.Second,
				ResponseHeaderTimeout: 5 * time.Second,
				ExpectContinueTimeout: 1 * time.Second,
			},
		},
		mirrors:       []string{defaultDebianMirror},
		suites:        defaultSuites,
		components:    defaultComponents,
		cache:         make(map[string]*SourceInfo),
		cachedSources: make(map[string]*CachedSources),
	}

	for _, opt := range opts {
		opt(r)
	}

	// Apply defaults if not set by options
	if len(r.mirrors) == 0 {
		r.mirrors = []string{defaultDebianMirror}
	}
	if len(r.suites) == 0 {
		r.suites = append([]string(nil), defaultSuites...)
	}
	if len(r.components) == 0 {
		r.components = append([]string(nil), defaultComponents...)
	}

	return r
}

func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
	pkg = strings.TrimSpace(strings.ToLower(pkg))
	if !packageNamePattern.MatchString(pkg) {
		return nil, fmt.Errorf("invalid package name %q", pkg)
	}

	// Проверяем кеш
	r.cacheMu.RLock()
	if cached, ok := r.cache[pkg]; ok {
		r.cacheMu.RUnlock()
		return cached, nil
	}
	r.cacheMu.RUnlock()

	// Only show debug for first lookup, not for dependencies
	if len(r.cache) == 0 {
		fmt.Printf("  [resolver] Looking up %s...\n", pkg)
	}
	start := time.Now()
	defer func() {
		if len(r.cache) == 0 {
			fmt.Printf("  [resolver] %s lookup took %v\n", pkg, time.Since(start))
		}
	}()

	checked := make([]string, 0, len(r.mirrors)*len(r.suites)*len(r.components))
	for _, mirror := range r.mirrors {
		for _, suite := range r.suites {
			for _, component := range r.components {
				if len(r.cache) == 0 {
					fmt.Printf("  [resolver] Checking %s/%s/%s...\n", mirror, suite, component)
				}
				record, err := r.findPackageInIndex(mirror, suite, component, pkg)
				checked = append(checked, fmt.Sprintf("%s:%s/%s", mirror, suite, component))
				if err != nil {
					if len(r.cache) == 0 {
						fmt.Printf("  [resolver] Not found in %s/%s/%s: %v\n", mirror, suite, component, err)
					}
					continue
				}

				result := &SourceInfo{
					RequestedPackage: pkg,
					SourcePackage:    record.Package,
					DSCURL:           strings.TrimRight(mirror, "/") + "/" + path.Join(record.Directory, record.DSCName),
					DSCSHA256:        record.DSCSHA256,
					DebianVersion:    record.Version,
					UpstreamVersion:  normalizeUpstreamVersion(record.Version),
					Suite:            suite,
					Component:        component,
					BuildDepends:     record.BuildDepends,
				}

				// Сохраняем в кеш
				r.cacheMu.Lock()
				r.cache[pkg] = result
				r.cacheMu.Unlock()

				if len(r.cache) == 1 {
					fmt.Printf("  [resolver] ✓ Found %s in %s/%s/%s\n", pkg, mirror, suite, component)
				}
				return result, nil
			}
		}
	}

	return nil, fmt.Errorf(
		"source package %s not found via HTTP (checked: %s)",
		pkg,
		strings.Join(checked, ", "),
	)
}

type sourceRecord struct {
	Package      string
	Version      string
	Directory    string
	DSCName      string
	DSCSHA256    string
	Binaries     []string
	BuildDepends []string
}

func (r *Resolver) findPackageInIndex(mirror, suite, component, pkg string) (*sourceRecord, error) {
	// First, ensure Sources file is loaded into memory
	cached, err := r.loadCachedSources(mirror, suite, component)
	if err != nil {
		return nil, err
	}

	// Lookup from in-memory map (O(1) instead of HTTP scan)
	if rec, found := r.lookupFromCache(pkg, cached); found {
		return rec, nil
	}

	return nil, fmt.Errorf("package not found in cache")
}

func (r *Resolver) findInSingleIndex(indexURL string, decoder func(io.Reader) (io.Reader, error), pkg string) (*sourceRecord, error) {
	resp, err := r.client.Get(indexURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("http %d", resp.StatusCode)
	}

	reader, err := decoder(resp.Body)
	if err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
	paragraph := make([]string, 0, 32)

	flush := func() (*sourceRecord, bool, error) {
		if len(paragraph) == 0 {
			return nil, false, nil
		}
		rec, err := parseSourcesParagraph(paragraph)
		paragraph = paragraph[:0]
		if err != nil {
			return nil, false, nil
		}
		if rec.Package == pkg || containsSourceBinary(rec.Binaries, pkg) {
			return &rec, true, nil
		}
		return nil, false, nil
	}

	for scanner.Scan() {
		line := scanner.Text()
		if strings.TrimSpace(line) == "" {
			rec, ok, err := flush()
			if err != nil {
				return nil, err
			}
			if ok {
				return rec, nil
			}
			continue
		}
		paragraph = append(paragraph, line)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}
	rec, ok, err := flush()
	if err != nil {
		return nil, err
	}
	if ok {
		return rec, nil
	}

	return nil, fmt.Errorf("package not found in %s", indexURL)
}

func buildSourcesURL(mirror, suite, component, ext string) string {
	mirror = strings.TrimRight(strings.TrimSpace(mirror), "/")
	suite = strings.Trim(strings.TrimSpace(suite), "/")
	component = strings.Trim(strings.TrimSpace(component), "/")

	name := "Sources"
	if ext != "" {
		name += "." + ext
	}

	return mirror + "/" + path.Join("dists", suite, component, "source", name)
}

func parseSourcesParagraph(lines []string) (sourceRecord, error) {
	fields := make(map[string]string)
	var currentKey string

	for _, raw := range lines {
		if strings.HasPrefix(raw, " ") || strings.HasPrefix(raw, "\t") {
			if currentKey == "" {
				continue
			}
			fields[currentKey] += "\n" + strings.TrimSpace(raw)
			continue
		}

		idx := strings.IndexByte(raw, ':')
		if idx <= 0 {
			continue
		}
		key := strings.TrimSpace(raw[:idx])
		value := strings.TrimSpace(raw[idx+1:])
		fields[key] = value
		currentKey = key
	}

	pkg := fields["Package"]
	ver := fields["Version"]
	dir := fields["Directory"]
	if pkg == "" || ver == "" || dir == "" {
		return sourceRecord{}, fmt.Errorf("missing required fields")
	}

	dscName, dscHash := parseChecksumsForDSC(fields["Checksums-Sha256"])
	if dscName == "" {
		return sourceRecord{}, fmt.Errorf("no dsc in checksums")
	}

	dir = strings.Trim(strings.TrimSpace(dir), "/")

	return sourceRecord{
		Package:      pkg,
		Version:      ver,
		Directory:    dir,
		DSCName:      dscName,
		DSCSHA256:    dscHash,
		Binaries:     parseCommaSeparatedField(fields["Binary"]),
		BuildDepends: parseCommaSeparatedField(fields["Build-Depends"]),
	}, nil
}

func parseCommaSeparatedField(raw string) []string {
	parts := strings.Split(strings.TrimSpace(raw), ",")
	out := make([]string, 0, len(parts))
	seen := make(map[string]struct{}, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(strings.ToLower(part))
		if part == "" {
			continue
		}
		if _, exists := seen[part]; exists {
			continue
		}
		seen[part] = struct{}{}
		out = append(out, part)
	}
	return out
}

func containsSourceBinary(names []string, wanted string) bool {
	wanted = strings.TrimSpace(strings.ToLower(wanted))
	if wanted == "" {
		return false
	}
	for _, name := range names {
		if strings.TrimSpace(strings.ToLower(name)) == wanted {
			return true
		}
	}
	return false
}

func parseChecksumsForDSC(raw string) (string, string) {
	for _, line := range strings.Split(raw, "\n") {
		fields := strings.Fields(strings.TrimSpace(line))
		if len(fields) < 3 {
			continue
		}
		name := fields[2]
		if strings.HasSuffix(strings.ToLower(name), ".dsc") {
			return name, strings.ToLower(fields[0])
		}
	}
	return "", ""
}

func decodeXZ(r io.Reader) (io.Reader, error) {
	return xz.NewReader(r)
}

func decodeGzip(r io.Reader) (io.Reader, error) {
	return gzip.NewReader(r)
}

func passthrough(r io.Reader) (io.Reader, error) {
	return r, nil
}

func normalizeUpstreamVersion(debianVersion string) string {
	v := strings.TrimSpace(debianVersion)
	if v == "" {
		return "0"
	}

	if idx := strings.IndexByte(v, ':'); idx >= 0 {
		v = v[idx+1:]
	}

	// Find the last dash that separates upstream version from Debian revision
	// For versions like "1.0-1-ubuntu1", we want to cut at the first dash to get "1.0"
	if idx := strings.Index(v, "-"); idx >= 0 {
		v = v[:idx]
	}

	replacer := strings.NewReplacer(
		"/", "_",
		":", "_",
		" ", "_",
	)
	v = strings.TrimSpace(replacer.Replace(v))
	if v == "" {
		return "0"
	}
	return v
}

func normalizeList(in []string) []string {
	set := make(map[string]struct{}, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		s = strings.TrimSpace(s)
		if s == "" {
			continue
		}
		if _, exists := set[s]; exists {
			continue
		}
		set[s] = struct{}{}
		out = append(out, s)
	}
	return out
}

// cacheDir returns the cache directory for zsvo
func cacheDir() string {
	if dir := os.Getenv("ZSVO_CACHE"); dir != "" {
		return dir
	}
	if home, err := os.UserHomeDir(); err == nil {
		return filepath.Join(home, ".cache", "zsvo")
	}
	return "/var/cache/zsvo"
}

// ensureCacheDir creates the cache directory if it doesn't exist
func ensureCacheDir() error {
	dir := cacheDir()
	return os.MkdirAll(dir, 0755)
}

// cachePath returns the path for a cached Sources file
func (r *Resolver) cachePath(mirror, suite, component string) string {
	host := strings.ReplaceAll(strings.TrimPrefix(mirror, "https://"), "/", "_")
	return filepath.Join(cacheDir(), fmt.Sprintf("Sources_%s_%s_%s.xz", host, suite, component))
}

// cacheKey returns a unique key for mirror/suite/component combination
func (r *Resolver) cacheKey(mirror, suite, component string) string {
	return fmt.Sprintf("%s:%s:%s", mirror, suite, component)
}

// loadCachedSources loads Sources.xz from cache or downloads it if not exists
func (r *Resolver) loadCachedSources(mirror, suite, component string) (*CachedSources, error) {
	key := r.cacheKey(mirror, suite, component)

	// Fast path: check if already loaded
	r.sourcesMu.RLock()
	if cached, exists := r.cachedSources[key]; exists {
		r.sourcesMu.RUnlock()
		return cached, nil
	}
	r.sourcesMu.RUnlock()

	// Slow path: load with write lock
	r.sourcesMu.Lock()
	defer r.sourcesMu.Unlock()

	// Double-check after acquiring write lock
	if cached, exists := r.cachedSources[key]; exists {
		return cached, nil
	}

	cachePath := r.cachePath(mirror, suite, component)
	cached := &CachedSources{
		packages: make(map[string]*sourceRecord),
		path:     cachePath,
	}

	// Try to load from disk cache first
	if _, err := os.Stat(cachePath); err == nil {
		// File exists on disk, parse it
		if err := r.parseSourcesFileToCache(cachePath, mirror, cached); err == nil {
			r.cachedSources[key] = cached
			return cached, nil
		}
	}

	// Download from HTTP
	url := buildSourcesURL(mirror, suite, component, "xz")
	if err := r.downloadSources(url, cachePath); err != nil {
		// Try gz
		url = buildSourcesURL(mirror, suite, component, "gz")
		cachePath = strings.TrimSuffix(cachePath, ".xz") + ".gz"
		if err := r.downloadSources(url, cachePath); err != nil {
			// Try uncompressed
			url = buildSourcesURL(mirror, suite, component, "")
			cachePath = strings.TrimSuffix(cachePath, ".gz")
			if err := r.downloadSources(url, cachePath); err != nil {
				return nil, err
			}
		}
	}

	// Parse the downloaded file
	if err := r.parseSourcesFileToCache(cachePath, mirror, cached); err != nil {
		return nil, err
	}

	r.cachedSources[key] = cached
	return cached, nil
}

// downloadSources downloads a Sources file from URL to local path
func (r *Resolver) downloadSources(url, localPath string) error {
	if len(r.cache) == 0 {
		fmt.Printf("  [resolver] Downloading %s...\n", url)
	}
	start := time.Now()

	resp, err := r.client.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("http %d", resp.StatusCode)
	}

	if err := ensureCacheDir(); err != nil {
		return err
	}

	file, err := os.Create(localPath)
	if err != nil {
		return err
	}
	defer file.Close()

	if _, err := io.Copy(file, resp.Body); err != nil {
		os.Remove(localPath)
		return err
	}

	if len(r.cache) == 0 {
		fmt.Printf("  [resolver] Downloaded in %v\n", time.Since(start))
	}
	return nil
}

// parseSourcesFileToCache parses a local Sources file into specific CachedSources instance
func (r *Resolver) parseSourcesFileToCache(path, mirror string, cached *CachedSources) error {
	if len(r.cache) == 0 {
		fmt.Printf("  [resolver] Parsing %s...\n", filepath.Base(path))
	}
	start := time.Now()

	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	var reader io.Reader = file

	// Decompress if needed
	if strings.HasSuffix(path, ".xz") {
		r, err := xz.NewReader(file)
		if err != nil {
			return err
		}
		reader = r
	} else if strings.HasSuffix(path, ".gz") {
		r, err := gzip.NewReader(file)
		if err != nil {
			return err
		}
		defer r.Close()
		reader = r
	}

	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)

	paragraph := make([]string, 0, 32)
	count := 0

	flush := func() {
		if len(paragraph) == 0 {
			return
		}
		rec, err := parseSourcesParagraph(paragraph)
		paragraph = paragraph[:0]
		if err != nil {
			return
		}

		cached.mu.Lock()
		cached.packages[rec.Package] = &rec
		// Also index by binary names
		for _, bin := range rec.Binaries {
			bin = strings.TrimSpace(strings.ToLower(bin))
			if bin != "" && bin != rec.Package {
				cached.packages[bin] = &rec
			}
		}
		cached.mu.Unlock()
		count++
	}

	for scanner.Scan() {
		line := scanner.Text()
		if strings.TrimSpace(line) == "" {
			flush()
			continue
		}
		paragraph = append(paragraph, line)
	}

	flush()

	if err := scanner.Err(); err != nil {
		return err
	}

	cached.path = path
	if len(r.cache) == 0 {
		fmt.Printf("  [resolver] Parsed %d packages in %v\n", count, time.Since(start))
	}
	return nil
}

// lookupFromCache searches for a package in the in-memory cache
func (r *Resolver) lookupFromCache(pkg string, cached *CachedSources) (*sourceRecord, bool) {
	cached.mu.RLock()
	defer cached.mu.RUnlock()

	if rec, ok := cached.packages[pkg]; ok {
		return rec, true
	}
	return nil, false
}
