// Copyright 2023 Volvo Car Corporation
// SPDX-License-Identifier: Apache-2.0

package terragen

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/golingon/lingon/pkg/internal/hcl"

	tfjson "github.com/hashicorp/terraform-json"
)

// TerraformVersions ...
type TerraformVersions struct {
	TerraformBlock TerraformBlock `hcl:"terraform,block"`
}

// TerraformBlock represents a terraform{} block in a Terraform stack
type TerraformBlock struct {
	RequiredProviders RequiredProviders `hcl:"required_providers,block"`
}

// RequiredProviders represents the map of required providers for a Terraform
// stack
type RequiredProviders struct {
	Providers map[string]Provider `hcl:",remain"`
}

// Provider represents a single element of a map of required providers
type Provider struct {
	Name    string // Lack of cty tag means it is ignored
	Source  string `cty:"source"`
	Version string `cty:"version"`
}

type generateOpts struct {
	cmd string
}

type GenerateOption func(*generateOpts)

func WithGenerateCmd(cmd string) GenerateOption {
	return func(opts *generateOpts) {
		opts.cmd = cmd
	}
}

// GenerateProviderSchema generates the schema for the given Terraform provider.
//
// The provider schema is generated by calling Terraform as follows:
//
//	terraform providers schema --json
//
// The JSON schema is then decoded and returned as a ProviderSchemas struct.
func GenerateProviderSchema(
	ctx context.Context,
	provider Provider,
	opts ...GenerateOption,
) (*tfjson.ProviderSchema, error) {
	opt := generateOpts{
		cmd: "tofu",
	}
	for _, o := range opts {
		o(&opt)
	}
	versions := TerraformVersions{
		TerraformBlock: TerraformBlock{
			RequiredProviders: RequiredProviders{
				Providers: map[string]Provider{
					provider.Name: {
						Source:  provider.Source,
						Version: provider.Version,
					},
				},
			},
		},
	}
	workingDir := filepath.Join(
		".lingon", "schemas", provider.Name,
		provider.Version,
	)
	if err := os.MkdirAll(workingDir, os.ModePerm); err != nil {
		return nil, fmt.Errorf(
			"creating schemas working directory: %s: %w",
			workingDir,
			err,
		)
	}

	tfVersionsFile := filepath.Join(workingDir, "versions.tf")
	f, err := os.Create(tfVersionsFile)
	if err != nil {
		return nil, fmt.Errorf("creating file %s: %w", tfVersionsFile, err)
	}
	if err := hcl.EncodeRaw(f, versions); err != nil {
		return nil, fmt.Errorf("encoding file %s: %w", tfVersionsFile, err)
	}

	if err := runTerraInit(ctx, workingDir, opt.cmd); err != nil {
		return nil, fmt.Errorf("running terra init: %w", err)
	}

	providersSchema, err := runTerraProvidersSchema(ctx, workingDir, opt.cmd)
	if err != nil {
		return nil, fmt.Errorf("running terra providers schema: %w", err)
	}

	return providerSchemaBySource(providersSchema, provider.Source)
}

// providerSchemaBySource returns the specific provider schema for the given
// source from the full list of provider schemas that is generated by terraform.
func providerSchemaBySource(
	schemas *tfjson.ProviderSchemas,
	source string,
) (*tfjson.ProviderSchema, error) {
	providerSchema, ok := schemas.Schemas[source]
	if !ok {
		findSourceBySuffix := func(
			providers map[string]*tfjson.ProviderSchema,
			source string,
		) (*tfjson.ProviderSchema, bool) {
			for src := range providers {
				if strings.HasSuffix(src, source) {
					return providers[src], true
				}
			}
			return nil, false
		}

		providerSchema, ok = findSourceBySuffix(schemas.Schemas, source)
		if !ok {
			return nil, fmt.Errorf(
				"provider source: %q: %w",
				source,
				ErrProviderSchemaNotFound,
			)
		}
	}
	return providerSchema, nil
}

func runTerraInit(
	ctx context.Context,
	dir string,
	cmd string,
) error {
	buf := bytes.Buffer{}
	terraCmd := exec.CommandContext(ctx, cmd, "init", "-upgrade")
	terraCmd.Dir = dir
	terraCmd.Stdout = &buf
	terraCmd.Stderr = &buf
	if err := terraCmd.Run(); err != nil {
		fmt.Println(buf.String())
		return fmt.Errorf(
			"running terra cmd: %q: %w",
			terraCmd.String(),
			err,
		)
	}

	return nil
}

func runTerraProvidersSchema(
	ctx context.Context,
	dir string,
	cmd string,
) (*tfjson.ProviderSchemas, error) {
	buf := bytes.Buffer{}
	terraCmd := exec.CommandContext(ctx, cmd, "providers", "schema", "-json")
	terraCmd.Dir = dir
	terraCmd.Stdout = &buf
	terraCmd.Stderr = os.Stderr
	if err := terraCmd.Run(); err != nil {
		return nil, fmt.Errorf(
			"running terra cmd: %q: %w",
			terraCmd.String(),
			err,
		)
	}

	var providerSchemas tfjson.ProviderSchemas
	dec := json.NewDecoder(&buf)
	dec.UseNumber()
	if err := dec.Decode(&providerSchemas); err != nil {
		return nil, fmt.Errorf("decoding providers schema: %w", err)
	}
	return &providerSchemas, nil
}
