// 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 scm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strings"

	"github.com/harness/gitness/git/command"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"
)

var (
	ErrNoDefaultBranch = errors.New("no default branch")
)

const devcontainerDefaultPath = ".devcontainer/devcontainer.json"

type SCM struct {
	scmProviderFactory Factory
}

func NewSCM(factory Factory) *SCM {
	return &SCM{scmProviderFactory: factory}
}

// CheckValidCodeRepo checks if the current URL is a valid and accessible code repo,
// input can be connector info, user token etc.
func (s *SCM) CheckValidCodeRepo(
	ctx context.Context,
	codeRepositoryRequest CodeRepositoryRequest,
) (*CodeRepositoryResponse, error) {
	codeRepositoryResponse := &CodeRepositoryResponse{
		URL:               codeRepositoryRequest.URL,
		CodeRepoIsPrivate: true,
	}

	branch, err := s.detectBranch(ctx, codeRepositoryRequest.URL)
	if err == nil {
		codeRepositoryResponse.Branch = branch
		codeRepositoryResponse.CodeRepoIsPrivate = false
		return codeRepositoryResponse, nil
	}

	scmProvider, err := s.getSCMProvider(codeRepositoryRequest.RepoType)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve SCM provider: %w", err)
	}

	resolvedCreds, err := s.resolveRepoCredentials(ctx, scmProvider, codeRepositoryRequest)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve repo credentials and URL: %w", err)
	}

	if branch, err = s.detectBranch(ctx, resolvedCreds.CloneURL.Value()); err == nil {
		codeRepositoryResponse.Branch = branch
	}
	return codeRepositoryResponse, nil
}

// GetSCMRepoDetails fetches repository name, credentials & devcontainer config file from the given repo and branch.
func (s *SCM) GetSCMRepoDetails(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
) (*ResolvedDetails, error) {
	scmProvider, err := s.getSCMProvider(gitspaceConfig.CodeRepo.Type)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve SCM provider: %w", err)
	}

	resolvedCredentials, err := scmProvider.ResolveCredentials(ctx, gitspaceConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve repo credentials and url: %w", err)
	}

	devcontainerConfig, err := s.getDevcontainerConfig(ctx, scmProvider, gitspaceConfig, resolvedCredentials)
	if err != nil {
		return nil, fmt.Errorf("failed to read or parse devcontainer config: %w", err)
	}
	var resolvedDetails = &ResolvedDetails{
		ResolvedCredentials: *resolvedCredentials,
		DevcontainerConfig:  devcontainerConfig,
	}
	return resolvedDetails, nil
}

func removeComments(input []byte) []byte {
	blockCommentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`)
	input = blockCommentRegex.ReplaceAll(input, nil)
	lineCommentRegex := regexp.MustCompile(`//.*`)
	return lineCommentRegex.ReplaceAll(input, nil)
}

func detectDefaultGitBranch(ctx context.Context, gitRepoDir string) (string, error) {
	cmd := command.New("ls-remote",
		command.WithFlag("--symref"),
		command.WithFlag("-q"),
		command.WithArg(gitRepoDir),
		command.WithArg("HEAD"),
	)
	output := &bytes.Buffer{}
	if err := cmd.Run(ctx, command.WithStdout(output)); err != nil {
		return "", fmt.Errorf("failed to ls remote repo")
	}
	var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
	match := lsRemoteHeadRegexp.FindStringSubmatch(strings.TrimSpace(output.String()))
	if match == nil {
		return "", ErrNoDefaultBranch
	}
	return match[1], nil
}

func (s *SCM) GetBranchURL(
	spacePath string,
	repoType enum.GitspaceCodeRepoType,
	repoURL string,
	branch string,
) (string, error) {
	scmProvider, err := s.scmProviderFactory.GetSCMProvider(repoType)
	if err != nil {
		return "", fmt.Errorf("failed to resolve scm provider while generating branch url: %w", err)
	}
	return scmProvider.GetBranchURL(spacePath, repoURL, branch)
}

// detectBranch tries to detect the default Git branch for a given URL.
func (s *SCM) detectBranch(ctx context.Context, repoURL string) (string, error) {
	defaultBranch, err := detectDefaultGitBranch(ctx, repoURL)
	if err != nil {
		return "", err
	}
	if defaultBranch == "" {
		return "main", nil
	}
	return defaultBranch, nil
}

func (s *SCM) getSCMProvider(repoType enum.GitspaceCodeRepoType) (Provider, error) {
	if repoType == "" {
		repoType = enum.CodeRepoTypeUnknown
	}
	return s.scmProviderFactory.GetSCMProvider(repoType)
}

func (s *SCM) resolveRepoCredentials(
	ctx context.Context,
	scmProvider Provider,
	codeRepositoryRequest CodeRepositoryRequest,
) (*ResolvedCredentials, error) {
	codeRepo := types.CodeRepo{URL: codeRepositoryRequest.URL}
	gitspaceUser := types.GitspaceUser{Identifier: codeRepositoryRequest.UserIdentifier}
	gitspaceConfig := types.GitspaceConfig{
		CodeRepo:     codeRepo,
		SpacePath:    codeRepositoryRequest.SpacePath,
		GitspaceUser: gitspaceUser,
	}
	return scmProvider.ResolveCredentials(ctx, gitspaceConfig)
}

func (s *SCM) getDevcontainerConfig(
	ctx context.Context,
	scmProvider Provider,
	gitspaceConfig types.GitspaceConfig,
	resolvedCredentials *ResolvedCredentials,
) (types.DevcontainerConfig, error) {
	config := types.DevcontainerConfig{}
	filePath := devcontainerDefaultPath
	catFileOutputBytes, err := scmProvider.GetFileContent(ctx, gitspaceConfig, filePath, resolvedCredentials)
	if err != nil {
		return config, fmt.Errorf("failed to read devcontainer file: %w", err)
	}

	if len(catFileOutputBytes) == 0 {
		return config, nil // Return an empty config if the file is empty
	}

	sanitizedJSON := removeComments(catFileOutputBytes)
	if err = json.Unmarshal(sanitizedJSON, &config); err != nil {
		return config, fmt.Errorf("failed to parse devcontainer JSON: %w", err)
	}

	return config, nil
}
