package store

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

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/infra/filestorage"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"gocloud.dev/blob"
)

const rootStorageTypeGit = "git"

var _ storageRuntime = &rootStorageGit{}

type rootStorageGit struct {
	settings *StorageGitConfig
	repo     *git.Repository
	root     string // repostitory root

	github *githubHelper
	meta   RootStorageMeta
	store  filestorage.FileStorage
}

func newGitStorage(meta RootStorageMeta, scfg RootStorageConfig, localWorkCache string) *rootStorageGit {
	cfg := scfg.Git
	if cfg == nil {
		cfg = &StorageGitConfig{}
	}
	scfg.Type = rootStorageTypeGit
	scfg.GCS = nil
	scfg.SQL = nil
	scfg.S3 = nil
	scfg.Git = cfg

	meta.Config = scfg
	if scfg.Prefix == "" {
		meta.Notice = append(meta.Notice, data.Notice{
			Severity: data.NoticeSeverityError,
			Text:     "Missing prefix",
		})
	}
	if cfg.Remote == "" {
		meta.Notice = append(meta.Notice, data.Notice{
			Severity: data.NoticeSeverityError,
			Text:     "Missing remote path configuration",
		})
	}

	if len(localWorkCache) < 2 {
		meta.Notice = append(meta.Notice, data.Notice{
			Severity: data.NoticeSeverityError,
			Text:     "Invalid local root folder",
		})
	}

	s := &rootStorageGit{
		settings: cfg,
	}
	if meta.Notice == nil {
		err := os.MkdirAll(localWorkCache, 0750)
		if err != nil {
			meta.Notice = append(meta.Notice, data.Notice{
				Severity: data.NoticeSeverityError,
				Text:     err.Error(),
			})
		}
	}

	if scfg.Disabled {
		meta.Notice = append(meta.Notice, data.Notice{
			Severity: data.NoticeSeverityWarning,
			Text:     "folder is disabled (in configuration)",
		})
	} else if setting.Env == setting.Prod {
		meta.Notice = append(meta.Notice, data.Notice{
			Severity: data.NoticeSeverityError,
			Text:     "git is only supported in dev mode (for now)",
		})
	}

	if meta.Notice == nil {
		repo, err := git.PlainOpen(localWorkCache)
		if errors.Is(err, git.ErrRepositoryNotExists) {
			repo, err = git.PlainClone(localWorkCache, false, &git.CloneOptions{
				URL:      cfg.Remote,
				Progress: os.Stdout,
				//Depth:    1,
				//SingleBranch: true,
			})
		}

		if err != nil {
			meta.Notice = append(meta.Notice, data.Notice{
				Severity: data.NoticeSeverityError,
				Text:     err.Error(),
			})
		}

		if err == nil {
			p := localWorkCache
			if cfg.Root != "" {
				p = filepath.Join(p, cfg.Root)
			}

			path := fmt.Sprintf("file://%s", p)
			bucket, err := blob.OpenBucket(context.Background(), path)
			if err != nil {
				grafanaStorageLogger.Warn("error loading storage", "prefix", scfg.Prefix, "err", err)
				meta.Notice = append(meta.Notice, data.Notice{
					Severity: data.NoticeSeverityError,
					Text:     "Failed to initialize storage",
				})
			} else {
				s.store = filestorage.NewCdkBlobStorage(
					grafanaStorageLogger,
					bucket, "", nil)

				meta.Ready = true // exists!
				s.root = p

				token := cfg.AccessToken
				if strings.HasPrefix(token, "$") {
					token = os.Getenv(token[1:])
					if token == "" {
						meta.Notice = append(meta.Notice, data.Notice{
							Severity: data.NoticeSeverityError,
							Text:     "Unable to find token environment variable: " + cfg.AccessToken,
						})
					}
				}

				if token != "" {
					s.github, err = newGithubHelper(context.Background(), cfg.Remote, token)
					if err != nil {
						meta.Notice = append(meta.Notice, data.Notice{
							Severity: data.NoticeSeverityError,
							Text:     "error creating github client: " + err.Error(),
						})
						s.github = nil
					} else {
						ghrepo, _, err := s.github.getRepo(context.Background())
						if err != nil {
							meta.Notice = append(meta.Notice, data.Notice{
								Severity: data.NoticeSeverityError,
								Text:     err.Error(),
							})
							s.github = nil
						} else {
							grafanaStorageLogger.Info("default branch", "branch", *ghrepo.DefaultBranch)
						}
					}
				}
			}
		}
		s.repo = repo

		// Try pulling after init
		if s.repo != nil && !scfg.Disabled {
			err = s.Sync()
			if err != nil {
				meta.Notice = append(meta.Notice, data.Notice{
					Severity: data.NoticeSeverityError,
					Text:     "unable to pull: " + err.Error(),
				})
			} else if cfg.PullInterval != "" {
				t, err := time.ParseDuration(cfg.PullInterval)
				if err != nil {
					meta.Notice = append(meta.Notice, data.Notice{
						Severity: data.NoticeSeverityError,
						Text:     "Invalid pull interval " + cfg.PullInterval,
					})
				} else {
					ticker := time.NewTicker(t)
					go func() {
						for range ticker.C {
							grafanaStorageLogger.Info("try git pull", "branch", s.settings.Remote)
							err = s.Sync()
							if err != nil {
								grafanaStorageLogger.Info("error pulling", "error", err)
							}
						}
					}()
				}
			}
		}
	}

	s.meta = meta
	return s
}

func (s *rootStorageGit) Meta() RootStorageMeta {
	return s.meta
}

func (s *rootStorageGit) Store() filestorage.FileStorage {
	return s.store
}

func (s *rootStorageGit) Pull() error {
	w, err := s.repo.Worktree()
	if err != nil {
		return err
	}

	err = w.Pull(&git.PullOptions{
		// Depth: 1,
		//SingleBranch: true,
	})
	if err != nil {
		return err
	}
	return nil
}

func (s *rootStorageGit) Write(ctx context.Context, cmd *WriteValueRequest) (*WriteValueResponse, error) {
	if s.github == nil {
		return nil, fmt.Errorf("github client not initialized")
	}
	// Write to the correct subfolder
	if s.settings.Root != "" {
		cmd.Path = s.settings.Root + cmd.Path
	}

	if cmd.Workflow == WriteValueWorkflow_PR {
		prcmd := makePRCommand{
			baseBranch: s.settings.Branch,
			headBranch: fmt.Sprintf("grafana_ui_%d", time.Now().UnixMilli()),
			title:      cmd.Title,
			body:       cmd.Message,
		}
		res := &WriteValueResponse{
			Branch: prcmd.headBranch,
		}

		ref, _, err := s.github.createRef(ctx, prcmd.baseBranch, prcmd.headBranch)
		if err != nil {
			res.Code = 500
			res.Message = "unable to create branch"
			return res, nil
		}

		err = s.github.pushCommit(ctx, ref, cmd)
		if err != nil {
			res.Code = 500
			res.Message = fmt.Sprintf("error creating commit: %s", err.Error())
			return res, nil
		}

		if prcmd.title == "" {
			prcmd.title = "Dashboard save: " + time.Now().String()
		}
		if prcmd.body == "" {
			prcmd.body = "Dashboard save: " + time.Now().String()
		}

		pr, _, err := s.github.createPR(ctx, prcmd)
		if err != nil {
			res.Code = 500
			res.Message = "error creating PR: " + err.Error()
			return res, nil
		}

		res.Code = 200
		res.URL = pr.GetHTMLURL()
		res.Pending = true
		res.Hash = *ref.Object.SHA
		res.Branch = prcmd.headBranch
		return res, nil
	}

	// Push to remote branch (save)
	if cmd.Workflow == WriteValueWorkflow_Push || true {
		res := &WriteValueResponse{
			Branch: s.settings.Branch,
		}
		ref, _, err := s.github.getRef(ctx, s.settings.Branch)
		if err != nil {
			res.Code = 500
			res.Message = "unable to create branch"
			return res, nil
		}
		err = s.github.pushCommit(ctx, ref, cmd)
		if err != nil {
			res.Code = 500
			res.Message = "error creating commit"
			return res, nil
		}
		ref, _, _ = s.github.getRef(ctx, s.settings.Branch)
		if ref != nil {
			res.Hash = *ref.Object.SHA
			res.URL = ref.GetURL()
		}

		err = s.Pull()
		if err != nil {
			res.Message = "error pulling: " + err.Error()
		}

		res.Code = 200
		return res, nil
	}

	rel := cmd.Path
	if s.meta.Config.Git.Root != "" {
		rel = filepath.Join(s.meta.Config.Git.Root, cmd.Path)
	}

	fpath := filepath.Join(s.root, rel)
	err := os.WriteFile(fpath, cmd.Body, 0644)
	if err != nil {
		return nil, err
	}

	w, err := s.repo.Worktree()
	if err != nil {
		return nil, err
	}

	// The file we just wrote
	_, err = w.Add(rel)
	if err != nil {
		return nil, err
	}

	msg := cmd.Message
	if msg == "" {
		msg = "changes from grafana ui"
	}
	usr := cmd.User
	if usr == nil {
		usr = &user.SignedInUser{}
	}

	hash, err := w.Commit(msg, &git.CommitOptions{
		Author: &object.Signature{
			Name:  firstRealString(usr.Name, usr.Login, usr.Email, "?"),
			Email: firstRealString(usr.Email, usr.Login, usr.Name, "?"),
			When:  time.Now(),
		},
	})
	if err != nil {
		return nil, err
	}

	grafanaStorageLogger.Info("made commit", "hash", hash)
	// err = s.repo.Push(&git.PushOptions{
	// 	InsecureSkipTLS: true,
	// })

	return &WriteValueResponse{
		Hash:    hash.String(),
		Message: "made commit",
	}, nil
}

func (s *rootStorageGit) Sync() error {
	grafanaStorageLogger.Info("GIT PULL", "remote", s.settings.Remote)
	err := s.Pull()
	if err != nil {
		if err.Error() == "already up-to-date" {
			return nil
		}
	}
	return err
}

func firstRealString(vals ...string) string {
	for _, v := range vals {
		if v != "" {
			return v
		}
	}
	return "?"
}
