// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package infrastructure

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/harness/gitness/infraprovider"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"
)

// Find finds the provisioned infra resources for the gitspace instance.
func (i InfraProvisioner) Find(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
	requiredGitspacePorts []types.GitspacePort,
) (*types.Infrastructure, error) {
	infraProviderResource := gitspaceConfig.InfraProviderResource
	infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource)
	if err != nil {
		return nil, err
	}

	infraProvider, err := i.getInfraProvider(infraProviderEntity.Type)
	if err != nil {
		return nil, err
	}

	var inputParams []types.InfraProviderParameter
	var agentPort = 0
	if infraProvider.ProvisioningType() == enum.InfraProvisioningTypeNew {
		inputParams, err = i.paramsForProvisioningTypeNew(ctx, gitspaceConfig)
		if err != nil {
			return nil, err
		}

		// TODO: What if the agent port has deviated from when the last instance was created?
		agentPort = i.config.AgentPort
	} else {
		inputParams, err = i.paramsForProvisioningTypeExisting(ctx, infraProviderResource, infraProvider)
		if err != nil {
			return nil, err
		}
	}

	infra, err := infraProvider.Find(ctx, gitspaceConfig.SpaceID, gitspaceConfig.SpacePath,
		gitspaceConfig.Identifier, gitspaceConfig.GitspaceInstance.Identifier,
		agentPort, requiredGitspacePorts, inputParams)
	if err != nil {
		return nil, fmt.Errorf("failed to find infrastructure: %w", err)
	}

	if infra == nil { // fallback
		infra, err = i.getInfraFromStoredInfo(ctx, gitspaceConfig)
		if err != nil {
			return nil, fmt.Errorf("failed to build infrastructure from stored info: %w", err)
		}
	}

	gitspaceScheme, err := getGitspaceScheme(gitspaceConfig.IDE, infraProviderResource.Metadata["gitspace_scheme"])
	if err != nil {
		return nil, fmt.Errorf("failed to get gitspace_scheme: %w", err)
	}
	infra.GitspaceScheme = gitspaceScheme
	return infra, nil
}

func (i InfraProvisioner) paramsForProvisioningTypeNew(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
) ([]types.InfraProviderParameter, error) {
	infraProvisionedLatest, err := i.infraProvisionedStore.FindLatestByGitspaceInstanceID(
		ctx, gitspaceConfig.GitspaceInstance.ID)
	if err != nil {
		return nil, fmt.Errorf(
			"could not find latest infra provisioned entity for instance %d: %w",
			gitspaceConfig.GitspaceInstance.ID, err)
	}
	if infraProvisionedLatest.InputParams == "" {
		return []types.InfraProviderParameter{}, err
	}
	allParams, err := deserializeInfraProviderParams(infraProvisionedLatest.InputParams)
	if err != nil {
		return nil, err
	}

	return allParams, nil
}

func deserializeInfraProviderParams(in string) ([]types.InfraProviderParameter, error) {
	var parameters []types.InfraProviderParameter
	err := json.Unmarshal([]byte(in), &parameters)
	if err != nil {
		return nil, fmt.Errorf("unable to unmarshal infra provider params %+v: %w", in, err)
	}
	return parameters, nil
}

func (i InfraProvisioner) paramsForProvisioningTypeExisting(
	ctx context.Context,
	infraProviderResource types.InfraProviderResource,
	infraProvider infraprovider.InfraProvider,
) ([]types.InfraProviderParameter, error) {
	allParams, err := i.getAllParamsFromDB(ctx, infraProviderResource, infraProvider)
	if err != nil {
		return nil, fmt.Errorf("could not get all params from DB while finding: %w", err)
	}

	return allParams, nil
}

func getGitspaceScheme(ideType enum.IDEType, gitspaceSchemeFromMetadata string) (string, error) {
	switch ideType {
	case enum.IDETypeVSCodeWeb:
		return gitspaceSchemeFromMetadata, nil
	case enum.IDETypeVSCode:
		return "ssh", nil
	case enum.IDETypeIntellij:
		return gitspaceSchemeFromMetadata, nil
	default:
		return "", fmt.Errorf("unknown ideType %s", ideType)
	}
}

func (i InfraProvisioner) getInfraFromStoredInfo(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
) (*types.Infrastructure, error) {
	infraProvisioned, err := i.infraProvisionedStore.FindLatestByGitspaceInstanceID(
		ctx,
		gitspaceConfig.GitspaceInstance.ID,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to find infraProvisioned: %w", err)
	}
	var infra types.Infrastructure
	err = json.Unmarshal([]byte(*infraProvisioned.ResponseMetadata), &infra)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal response metadata: %w", err)
	}
	return &infra, nil
}

// Methods to find infra provider resources.
func (i InfraProvisioner) getConfigFromResource(
	ctx context.Context,
	infraProviderResource types.InfraProviderResource,
) (*types.InfraProviderConfig, error) {
	config, err := i.infraProviderConfigStore.Find(ctx, infraProviderResource.InfraProviderConfigID)
	if err != nil {
		return nil, fmt.Errorf(
			"unable to get infra provider details for ID %d: %w",
			infraProviderResource.InfraProviderConfigID, err)
	}
	return config, nil
}

func (i InfraProvisioner) getInfraProvider(
	infraProviderType enum.InfraProviderType,
) (infraprovider.InfraProvider, error) {
	infraProvider, err := i.providerFactory.GetInfraProvider(infraProviderType)
	if err != nil {
		return nil, fmt.Errorf("unable to get infra provider of type %v: %w", infraProviderType, err)
	}
	return infraProvider, nil
}

func (i InfraProvisioner) getAllParamsFromDB(
	ctx context.Context,
	infraProviderResource types.InfraProviderResource,
	infraProvider infraprovider.InfraProvider,
) ([]types.InfraProviderParameter, error) {
	var allParams []types.InfraProviderParameter

	templateParams, err := i.getTemplateParams(ctx, infraProvider, infraProviderResource)
	if err != nil {
		return nil, err
	}

	allParams = append(allParams, templateParams...)

	params := i.paramsFromResource(infraProviderResource, infraProvider)

	allParams = append(allParams, params...)

	return allParams, nil
}

func (i InfraProvisioner) getTemplateParams(
	ctx context.Context,
	infraProvider infraprovider.InfraProvider,
	infraProviderResource types.InfraProviderResource,
) ([]types.InfraProviderParameter, error) {
	var params []types.InfraProviderParameter
	templateParams := infraProvider.TemplateParams()

	for _, param := range templateParams {
		key := param.Name
		if infraProviderResource.Metadata[key] != "" {
			templateIdentifier := infraProviderResource.Metadata[key]

			template, err := i.infraProviderTemplateStore.FindByIdentifier(
				ctx, infraProviderResource.SpaceID, templateIdentifier)
			if err != nil {
				return nil, fmt.Errorf("unable to get template params for ID %s: %w",
					infraProviderResource.Metadata[key], err)
			}

			params = append(params, types.InfraProviderParameter{
				Name:  key,
				Value: template.Data,
			})
		}
	}

	return params, nil
}

func (i InfraProvisioner) paramsFromResource(
	infraProviderResource types.InfraProviderResource,
	infraProvider infraprovider.InfraProvider,
) []types.InfraProviderParameter {
	// NOTE: templateParamsMap is required to filter out template params since their values have already been fetched
	// and we dont need the template identifiers, which are the values for template params in the resource Metadata.
	templateParamsMap := make(map[string]bool)
	for _, templateParam := range infraProvider.TemplateParams() {
		templateParamsMap[templateParam.Name] = true
	}

	params := make([]types.InfraProviderParameter, 0)

	for key, value := range infraProviderResource.Metadata {
		if key == "" || value == "" || templateParamsMap[key] {
			continue
		}
		params = append(params, types.InfraProviderParameter{
			Name:  key,
			Value: value,
		})
	}
	return params
}
