// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
	"archive/tar"
	"bufio"
	"bytes"
	"compress/gzip"
	"io"
	"regexp"
	"strconv"
	"strings"

	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/modules/validation"

	"github.com/klauspost/compress/zstd"
	"github.com/ulikunitz/xz"
)

const (
	PropertyRepository   = "arch.repository"
	PropertyArchitecture = "arch.architecture"
	PropertySignature    = "arch.signature"
	PropertyMetadata     = "arch.metadata"

	SettingKeyPrivate = "arch.key.private"
	SettingKeyPublic  = "arch.key.public"

	RepositoryPackage = "_arch"
	RepositoryVersion = "_repository"

	AnyArch = "any"
)

var (
	ErrMissingPKGINFOFile  = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
	ErrUnsupportedFormat   = util.NewInvalidArgumentErrorf("unsupported package container format")
	ErrInvalidName         = util.NewInvalidArgumentErrorf("package name is invalid")
	ErrInvalidVersion      = util.NewInvalidArgumentErrorf("package version is invalid")
	ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")

	// https://man.archlinux.org/man/PKGBUILD.5
	namePattern    = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
	versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
)

type Package struct {
	Name                     string
	Version                  string
	VersionMetadata          VersionMetadata
	FileMetadata             FileMetadata
	FileCompressionExtension string
}

type VersionMetadata struct {
	Description string   `json:"description,omitempty"`
	ProjectURL  string   `json:"project_url,omitempty"`
	Licenses    []string `json:"licenses,omitempty"`
}

type FileMetadata struct {
	Architecture  string   `json:"architecture"`
	Base          string   `json:"base,omitempty"`
	InstalledSize int64    `json:"installed_size,omitempty"`
	BuildDate     int64    `json:"build_date,omitempty"`
	Packager      string   `json:"packager,omitempty"`
	Groups        []string `json:"groups,omitempty"`
	Provides      []string `json:"provides,omitempty"`
	Depends       []string `json:"depends,omitempty"`
	OptDepends    []string `json:"opt_depends,omitempty"`
	MakeDepends   []string `json:"make_depends,omitempty"`
	CheckDepends  []string `json:"check_depends,omitempty"`
	XData         []string `json:"xdata,omitempty"`
	Backup        []string `json:"backup,omitempty"`
	Files         []string `json:"files,omitempty"`
}

// ParsePackage parses an Arch package file
func ParsePackage(r io.Reader) (*Package, error) {
	header := make([]byte, 10)
	n, err := util.ReadAtMost(r, header)
	if err != nil {
		return nil, err
	}

	r = io.MultiReader(bytes.NewReader(header[:n]), r)

	var inner io.Reader
	var compressionType string
	if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst
		zr, err := zstd.NewReader(r)
		if err != nil {
			return nil, err
		}
		defer zr.Close()

		inner = zr
		compressionType = "zst"
	} else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz
		xzr, err := xz.NewReader(r)
		if err != nil {
			return nil, err
		}

		inner = xzr
		compressionType = "xz"
	} else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz
		gzr, err := gzip.NewReader(r)
		if err != nil {
			return nil, err
		}
		defer gzr.Close()

		inner = gzr
		compressionType = "gz"
	} else {
		return nil, ErrUnsupportedFormat
	}

	var p *Package
	files := make([]string, 0, 10)

	tr := tar.NewReader(inner)
	for {
		hd, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}

		if hd.Typeflag != tar.TypeReg {
			continue
		}

		filename := hd.FileInfo().Name()
		if filename == ".PKGINFO" {
			p, err = ParsePackageInfo(tr)
			if err != nil {
				return nil, err
			}
		} else if !strings.HasPrefix(filename, ".") {
			files = append(files, hd.Name)
		}
	}

	if p == nil {
		return nil, ErrMissingPKGINFOFile
	}

	p.FileMetadata.Files = files
	p.FileCompressionExtension = compressionType

	return p, nil
}

// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata
// https://man.archlinux.org/man/PKGBUILD.5
// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161
func ParsePackageInfo(r io.Reader) (*Package, error) {
	p := &Package{}

	s := bufio.NewScanner(r)
	for s.Scan() {
		line := s.Text()

		if strings.HasPrefix(line, "#") {
			continue
		}

		i := strings.IndexRune(line, '=')
		if i == -1 {
			continue
		}

		key := strings.TrimSpace(line[:i])
		value := strings.TrimSpace(line[i+1:])

		switch key {
		case "pkgname":
			p.Name = value
		case "pkgbase":
			p.FileMetadata.Base = value
		case "pkgver":
			p.Version = value
		case "pkgdesc":
			p.VersionMetadata.Description = value
		case "url":
			p.VersionMetadata.ProjectURL = value
		case "packager":
			p.FileMetadata.Packager = value
		case "arch":
			p.FileMetadata.Architecture = value
		case "license":
			p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value)
		case "provides":
			p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
		case "depend":
			p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
		case "optdepend":
			p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
		case "makedepend":
			p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
		case "checkdepend":
			p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
		case "backup":
			p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
		case "group":
			p.FileMetadata.Groups = append(p.FileMetadata.Groups, value)
		case "builddate":
			date, err := strconv.ParseInt(value, 10, 64)
			if err != nil {
				return nil, err
			}
			p.FileMetadata.BuildDate = date
		case "size":
			size, err := strconv.ParseInt(value, 10, 64)
			if err != nil {
				return nil, err
			}
			p.FileMetadata.InstalledSize = size
		case "xdata":
			p.FileMetadata.XData = append(p.FileMetadata.XData, value)
		}
	}
	if err := s.Err(); err != nil {
		return nil, err
	}

	if !namePattern.MatchString(p.Name) {
		return nil, ErrInvalidName
	}
	if !versionPattern.MatchString(p.Version) {
		return nil, ErrInvalidVersion
	}
	if p.FileMetadata.Architecture == "" {
		return nil, ErrInvalidArchitecture
	}

	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
		p.VersionMetadata.ProjectURL = ""
	}

	return p, nil
}
