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

package pub

import (
	"archive/tar"
	"compress/gzip"
	"io"
	"regexp"
	"strings"

	"forgejo.org/modules/util"
	"forgejo.org/modules/validation"

	"github.com/hashicorp/go-version"
	"go.yaml.in/yaml/v3"
)

var (
	ErrMissingPubspecFile  = util.NewInvalidArgumentErrorf("Pubspec file is missing")
	ErrPubspecFileTooLarge = util.NewInvalidArgumentErrorf("Pubspec file is too large")
	ErrInvalidName         = util.NewInvalidArgumentErrorf("package name is invalid")
	ErrInvalidVersion      = util.NewInvalidArgumentErrorf("package version is invalid")
)

var namePattern = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)

// https://github.com/dart-lang/pub-dev/blob/4d582302a8d10152a5cd6129f65bf4f4dbca239d/pkg/pub_package_reader/lib/pub_package_reader.dart#L143
const maxPubspecFileSize = 128 * 1024

// Package represents a Pub package
type Package struct {
	Name     string
	Version  string
	Metadata *Metadata
}

// Metadata represents the metadata of a Pub package
type Metadata struct {
	Description      string `json:"description,omitempty"`
	ProjectURL       string `json:"project_url,omitempty"`
	RepositoryURL    string `json:"repository_url,omitempty"`
	DocumentationURL string `json:"documentation_url,omitempty"`
	Readme           string `json:"readme,omitempty"`
	Pubspec          any    `json:"pubspec"`
}

type pubspecPackage struct {
	Name          string `yaml:"name"`
	Version       string `yaml:"version"`
	Description   string `yaml:"description"`
	Homepage      string `yaml:"homepage"`
	Repository    string `yaml:"repository"`
	Documentation string `yaml:"documentation"`
}

// ParsePackage parses the Pub package file
func ParsePackage(r io.Reader) (*Package, error) {
	gzr, err := gzip.NewReader(r)
	if err != nil {
		return nil, err
	}
	defer gzr.Close()

	var p *Package
	var readme string

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

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

		if hd.Name == "pubspec.yaml" {
			if hd.Size > maxPubspecFileSize {
				return nil, ErrPubspecFileTooLarge
			}
			p, err = ParsePubspecMetadata(tr)
			if err != nil {
				return nil, err
			}
		} else if strings.ToLower(hd.Name) == "readme.md" {
			data, err := io.ReadAll(tr)
			if err != nil {
				return nil, err
			}
			readme = string(data)
		}
	}

	if p == nil {
		return nil, ErrMissingPubspecFile
	}

	p.Metadata.Readme = readme

	return p, nil
}

// ParsePubspecMetadata parses a Pubspec file to retrieve the metadata of a Pub package
func ParsePubspecMetadata(r io.Reader) (*Package, error) {
	buf, err := io.ReadAll(io.LimitReader(r, maxPubspecFileSize))
	if err != nil {
		return nil, err
	}

	var p pubspecPackage
	if err := yaml.Unmarshal(buf, &p); err != nil {
		return nil, err
	}

	if !namePattern.MatchString(p.Name) {
		return nil, ErrInvalidName
	}

	v, err := version.NewSemver(p.Version)
	if err != nil {
		return nil, ErrInvalidVersion
	}

	if !validation.IsValidURL(p.Homepage) {
		p.Homepage = ""
	}
	if !validation.IsValidURL(p.Repository) {
		p.Repository = ""
	}

	var pubspec any
	if err := yaml.Unmarshal(buf, &pubspec); err != nil {
		return nil, err
	}

	return &Package{
		Name:    p.Name,
		Version: v.String(),
		Metadata: &Metadata{
			Description:      p.Description,
			ProjectURL:       p.Homepage,
			RepositoryURL:    p.Repository,
			DocumentationURL: p.Documentation,
			Pubspec:          pubspec,
		},
	}, nil
}
