package commands

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	hcversion "github.com/hashicorp/go-version"
	"github.com/pelletier/go-toml/v2"
	"github.com/santhosh-tekuri/jsonschema/v5"
	_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"gopkg.in/yaml.v3"

	"github.com/golangci/golangci-lint/pkg/exitcodes"
)

type verifyOptions struct {
	schemaURL string // For debugging purpose only (Flag only).
}

func (c *configCommand) executeVerify(cmd *cobra.Command, _ []string) error {
	usedConfigFile := c.getUsedConfig()
	if usedConfigFile == "" {
		c.log.Warnf("No config file detected")
		os.Exit(exitcodes.NoConfigFileDetected)
	}

	schemaURL, err := createSchemaURL(cmd.Flags(), c.buildInfo)
	if err != nil {
		return fmt.Errorf("get JSON schema: %w", err)
	}

	err = validateConfiguration(schemaURL, usedConfigFile)
	if err != nil {
		var v *jsonschema.ValidationError
		if !errors.As(err, &v) {
			return fmt.Errorf("[%s] validate: %w", usedConfigFile, err)
		}

		detail := v.DetailedOutput()

		printValidationDetail(cmd, &detail)

		return fmt.Errorf("the configuration contains invalid elements")
	}

	return nil
}

func createSchemaURL(flags *pflag.FlagSet, buildInfo BuildInfo) (string, error) {
	schemaURL, err := flags.GetString("schema")
	if err != nil {
		return "", fmt.Errorf("get schema flag: %w", err)
	}

	if schemaURL != "" {
		return schemaURL, nil
	}

	switch {
	case buildInfo.Version != "" && buildInfo.Version != "(devel)":
		version, err := hcversion.NewVersion(buildInfo.Version)
		if err != nil {
			return "", fmt.Errorf("parse version: %w", err)
		}

		schemaURL = fmt.Sprintf("https://golangci-lint.run/jsonschema/golangci.v%d.%d.jsonschema.json",
			version.Segments()[0], version.Segments()[1])

	case buildInfo.Commit != "" && buildInfo.Commit != "?":
		if buildInfo.Commit == "unknown" {
			return "", errors.New("unknown commit information")
		}

		commit := buildInfo.Commit

		if strings.HasPrefix(commit, "(") {
			c, _, ok := strings.Cut(strings.TrimPrefix(commit, "("), ",")
			if !ok {
				return "", errors.New("commit information not found")
			}

			commit = c
		}

		schemaURL = fmt.Sprintf("https://raw.githubusercontent.com/golangci/golangci-lint/%s/jsonschema/golangci.next.jsonschema.json",
			commit)

	default:
		return "", errors.New("version not found")
	}

	return schemaURL, nil
}

func validateConfiguration(schemaPath, targetFile string) error {
	compiler := jsonschema.NewCompiler()
	compiler.Draft = jsonschema.Draft7

	schema, err := compiler.Compile(schemaPath)
	if err != nil {
		return fmt.Errorf("compile schema: %w", err)
	}

	var m any

	switch strings.ToLower(filepath.Ext(targetFile)) {
	case ".yaml", ".yml", ".json":
		m, err = decodeYamlFile(targetFile)
		if err != nil {
			return err
		}

	case ".toml":
		m, err = decodeTomlFile(targetFile)
		if err != nil {
			return err
		}

	default:
		// unsupported
		return errors.New("unsupported configuration format")
	}

	return schema.Validate(m)
}

func printValidationDetail(cmd *cobra.Command, detail *jsonschema.Detailed) {
	if detail.Error != "" {
		cmd.PrintErrf("jsonschema: %q does not validate with %q: %s\n",
			strings.ReplaceAll(strings.TrimPrefix(detail.InstanceLocation, "/"), "/", "."), detail.KeywordLocation, detail.Error)
	}

	for _, d := range detail.Errors {
		d := d
		printValidationDetail(cmd, &d)
	}
}

func decodeYamlFile(filename string) (any, error) {
	file, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("[%s] file open: %w", filename, err)
	}

	defer func() { _ = file.Close() }()

	var m any
	err = yaml.NewDecoder(file).Decode(&m)
	if err != nil {
		return nil, fmt.Errorf("[%s] YAML decode: %w", filename, err)
	}

	return m, nil
}

func decodeTomlFile(filename string) (any, error) {
	file, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("[%s] file open: %w", filename, err)
	}

	defer func() { _ = file.Close() }()

	var m any
	err = toml.NewDecoder(file).Decode(&m)
	if err != nil {
		return nil, fmt.Errorf("[%s] TOML decode: %w", filename, err)
	}

	return m, nil
}
