package sborepo

import (
	"bufio"
	"fmt"
	"io"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"gitlab.com/M0M097/pkg/internal/filecheck"
	"gitlab.com/M0M097/pkg/internal/must"
)

// Package represents a Slackware package. ParentPath is the category directory
// containing the package directory; Path computes ParentPath/Name.
type Package struct {
	Name       PkgName
	ParentPath string
}

// Dependencies returns a slice of dependencies for the package as defined in
// the REQUIRES field of the package's .info file.
func (pkg *Package) Dependencies() []string { return strings.Fields(pkg.queryInfoFile("REQUIRES")) }

// InfoFile returns the path to the package's .info file.
func (pkg *Package) InfoFile() string { return filepath.Join(pkg.Path(), pkg.Name+".info") }

// Path returns the full path to the package directory.
func (pkg *Package) Path() string { return filepath.Join(pkg.ParentPath, pkg.Name) }

// Readme returns the full path to the README file in the package directory.
func (pkg *Package) Readme() string { return filepath.Join(pkg.Path(), "README") }

// SlackBuild returns the full path to the SlackBuild script for the package.
func (pkg *Package) SlackBuild() string { return filepath.Join(pkg.Path(), pkg.Name+".SlackBuild") }

// Version returns the version of the package as defined in the VERSION field
// of the package's .info file.
func (pkg *Package) Version() string { return pkg.queryInfoFile("VERSION") }

// FullName returns the full name of the package, which follows the convention
// of "name-version".
func (pkg *Package) FullName() string { return pkg.Name + "-" + pkg.Version() }

// SlackDesc returns the full path to the slack-desc file in the package.
func (pkg *Package) SlackDesc() string { return filepath.Join(pkg.Path(), "slack-desc") }

// IsReadmeRequired returns true if the package requires the README file to be read.
func (pkg *Package) IsReadmeRequired() bool {
	matched, err := regexp.MatchString("%README%", pkg.queryInfoFile("REQUIRES"))
	return matched && err == nil
}

// AllReadmes returns a space-separated list of the full paths to all README
// files in the package directory as a string.
func (pkg *Package) AllReadmes() string {
	sb, re := strings.Builder{}, regexp.MustCompile("README")

	for _, file := range must.ReadDir(pkg.Path()) {
		fn := file.Name()
		if re.MatchString(fn) {
			sb.WriteString(filepath.Join(pkg.Path(), fn) + " ")
		}
	}

	return sb.String()[:sb.Len()-1]
}

// DownloadURLs returns a slice of download URLs for the package. It first
// checks for the x86_64 specific download URL and falls back to the generic
// download URL if the x86_64 specific one is not found.
func (pkg *Package) DownloadURLs() []string {
	download_x86_64 := pkg.queryInfoFile("DOWNLOAD_x86_64")
	if download_x86_64 != "" {
		return strings.Fields(download_x86_64)
	}
	return strings.Fields(pkg.queryInfoFile("DOWNLOAD"))
}

// MD5Sums returns a slice of MD5 checksums for the package. It first checks for
// the x86_64 specific MD5 checksum and falls back to the generic MD5 checksum
// if the x86_64 specific one is not found.
func (pkg *Package) MD5Sums() []string {
	md5sum_x86_64 := pkg.queryInfoFile("MD5SUM_x86_64")
	if md5sum_x86_64 != "" {
		return strings.Fields(md5sum_x86_64)
	}
	return strings.Fields(pkg.queryInfoFile("MD5SUM"))
}

// DownloadNames returns a slice of download file names for the package. It
// extracts the base name from each download URL returned by DownloadURLs.
func (pkg *Package) DownloadNames() []string {
	urls := pkg.DownloadURLs()
	names := make([]string, len(urls))
	for i, url := range urls {
		names[i] = filepath.Base(url)
	}
	return names
}

// IsUpToDate checks if the package is up to date by comparing the SlackBuild
// script in the package directory with the one in the documentation directory.
func (pkg *Package) IsUpToDate(docDir, kernelVersion string) bool {
	var (
		fullPath  = filepath.Join(docDir, pkg.FullName())
		sbPath    = pkg.SlackBuild()
		sbName    = pkg.Name + ".SlackBuild"
		sbPathDoc = filepath.Join(fullPath, sbName)
	)
	isEqual, err := filecheck.EqualContent(sbPath, sbPathDoc)
	if err == nil {
		return isEqual
	}
	sbPathDoc = filepath.Join(fullPath+"_"+kernelVersion, sbName)
	isEqual, _ = filecheck.EqualContent(sbPath, sbPathDoc)
	return isEqual
}

func (pkg *Package) queryInfoFile(key string) string {
	file := must.Open(pkg.InfoFile())
	defer file.Close()

	for scanner := bufio.NewScanner(file); scanner.Scan(); {
		if s := strings.Split(scanner.Text(), "\""); s[0] == key+"=" {
			for strings.HasSuffix(s[1], "\\") { // line continuation
				scanner.Scan()
				s[1] = s[1][:len(s[1])-1] + strings.Trim(scanner.Text(), "\"")
			}
			return s[1]
		}
	}

	return ""
}

// FindBuildPkg searches for the latest build package in the given temporary
// directory that matches the package's full name and returns its full path.
func (pkg *Package) FindBuildPkg(tmpDir string) string {
	var (
		re               = regexp.MustCompile(pkg.FullName() + ".*_SBo.*")
		modificationTime time.Time
		pkgName          string
	)
	for _, slackpkg := range must.ReadDir(tmpDir) {
		if re.MatchString(slackpkg.Name()) {
			if t := must.ModTime(slackpkg); t.After(modificationTime) {
				modificationTime = t
				pkgName = slackpkg.Name()
			}
		}
	}
	if pkgName == "" {
		panic(pkg.Name + ": No package to install")
	}
	return filepath.Join(tmpDir, pkgName)
}

// Description returns the name plus the description of the package as it is
// found in the first line of the slack-desc file.
func (pkg *Package) Description() string {
	file := must.Open(pkg.SlackDesc())
	defer file.Close()

	prefix := pkg.Name + ": "
	lenPrefix := len(prefix)

	for scanner := bufio.NewScanner(file); scanner.Scan(); {
		if line := scanner.Text(); strings.HasPrefix(line, prefix) {
			return line[lenPrefix:]
		}
	}

	// We fallback to the name if for some reason the slack-desc file is
	// ill-formatted.
	return pkg.Name
}

// Write writes the package name to the given io.Writer.
func (pkg *Package) Write(w io.Writer) { fmt.Fprintln(w, pkg.Name) }

// PackageNameFromFullName converts a full package name such as
// - "yt-dlp-2025.05.22"  -> "yt-dlp"
// - "pkg-foo-1.0"        -> "pkg-foo"
// - "pkg-1.0_kernel5.10.0_1.0.0_amd64" -> "pkg"
// It returns an empty string when no version part can be detected.
func PackageNameFromFullName(fullName string) string {
	if i := strings.LastIndex(fullName, "-"); i != -1 {
		return fullName[:i]
	}
	return ""
}
