package hosting_service

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/go-errors/errors"
	"github.com/jesseduffield/lazygit/pkg/i18n"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/sirupsen/logrus"
)

// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
// and this package's responsibility is to determine which service you're using based on the remote URL,
// and then which URL you need for whatever use case you have.

type HostingServiceMgr struct {
	log       logrus.FieldLogger
	tr        *i18n.TranslationSet
	remoteURL string // e.g. https://github.com/jesseduffield/lazygit

	// see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
	configServiceDomains map[string]string
}

// NewHostingServiceMgr creates new instance of PullRequest
func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr {
	return &HostingServiceMgr{
		log:                  log,
		tr:                   tr,
		remoteURL:            remoteURL,
		configServiceDomains: configServiceDomains,
	}
}

func (self *HostingServiceMgr) GetPullRequestURL(from string, to string) (string, error) {
	gitService, err := self.getService()
	if err != nil {
		return "", err
	}

	if to == "" {
		return gitService.getPullRequestURLIntoDefaultBranch(from), nil
	} else {
		return gitService.getPullRequestURLIntoTargetBranch(from, to), nil
	}
}

func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
	gitService, err := self.getService()
	if err != nil {
		return "", err
	}

	pullRequestURL := gitService.getCommitURL(commitSha)

	return pullRequestURL, nil
}

func (self *HostingServiceMgr) getService() (*Service, error) {
	serviceDomain, err := self.getServiceDomain(self.remoteURL)
	if err != nil {
		return nil, err
	}

	root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL)
	if err != nil {
		return nil, err
	}

	return &Service{
		root:              root,
		ServiceDefinition: serviceDomain.serviceDefinition,
	}, nil
}

func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) {
	candidateServiceDomains := self.getCandidateServiceDomains()

	for _, serviceDomain := range candidateServiceDomains {
		// I feel like it makes more sense to see if the repo url contains the service domain's git domain,
		// but I don't want to break anything by changing that right now.
		if strings.Contains(repoURL, serviceDomain.serviceDefinition.provider) {
			return &serviceDomain, nil
		}
	}

	return nil, errors.New(self.tr.UnsupportedGitService)
}

func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
	serviceDefinitionByProvider := map[string]ServiceDefinition{}
	for _, serviceDefinition := range serviceDefinitions {
		serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
	}

	var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
	copy(serviceDomains, defaultServiceDomains)

	if len(self.configServiceDomains) > 0 {
		for gitDomain, typeAndDomain := range self.configServiceDomains {
			splitData := strings.Split(typeAndDomain, ":")
			if len(splitData) != 2 {
				self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain)
				continue
			}

			provider := splitData[0]
			webDomain := splitData[1]

			serviceDefinition, ok := serviceDefinitionByProvider[provider]
			if !ok {
				providerNames := []string{}
				for _, serviceDefinition := range serviceDefinitions {
					providerNames = append(providerNames, serviceDefinition.provider)
				}
				self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", "))
				continue
			}

			serviceDomains = append(serviceDomains, ServiceDomain{
				gitDomain:         gitDomain,
				webDomain:         webDomain,
				serviceDefinition: serviceDefinition,
			})
		}
	}

	return serviceDomains
}

// a service domains pairs a service definition with the actual domain it's being served from.
// Sometimes the git service is hosted in a custom domains so although it'll use say
// the github service definition, it'll actually be served from e.g. my-custom-github.com
type ServiceDomain struct {
	gitDomain         string // the one that appears in the git remote url
	webDomain         string // the one that appears in the web url
	serviceDefinition ServiceDefinition
}

func (self ServiceDomain) getRootFromRemoteURL(repoURL string) (string, error) {
	// we may want to make this more specific to the service in future e.g. if
	// some new service comes along which has a different root url structure.
	repoInfo, err := self.serviceDefinition.getRepoInfoFromURL(repoURL)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("https://%s/%s/%s", self.webDomain, repoInfo.Owner, repoInfo.Repository), nil
}

// RepoInformation holds some basic information about the repo
type RepoInformation struct {
	Owner      string
	Repository string
}

type ServiceDefinition struct {
	provider                        string
	pullRequestURLIntoDefaultBranch string
	pullRequestURLIntoTargetBranch  string
	commitURL                       string
	regexStrings                    []string
}

func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, error) {
	for _, regexStr := range self.regexStrings {
		re := regexp.MustCompile(regexStr)
		matches := utils.FindNamedMatches(re, url)
		if matches != nil {
			return &RepoInformation{
				Owner:      matches["owner"],
				Repository: matches["repo"],
			}, nil
		}
	}

	return nil, errors.New("Failed to parse repo information from url")
}

type Service struct {
	root string
	ServiceDefinition
}

func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string {
	return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from})
}

func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string {
	return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to})
}

func (self *Service) getCommitURL(commitSha string) string {
	return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha})
}

func (self *Service) resolveUrl(templateString string, args map[string]string) string {
	return self.root + utils.ResolvePlaceholderString(templateString, args)
}
