package models

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

const (
	tree = "tree"
	blob = "blob"
	defaultDesc = "Unnamed repository; edit this file 'description' to name the repository.\n"
)

type GitObject struct {
	ObjectType string
	Path       string
}

type ObjectSort []GitObject

func (o ObjectSort) Len() int      { return len(o) }
func (o ObjectSort) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o ObjectSort) Less(i, j int) bool {
	if o[i].ObjectType == tree && o[j].ObjectType == blob {
		return true
	}

	if o[j].ObjectType == tree && o[i].ObjectType == blob {
		return false
	}

	// Same ObjectType
	return o[i].Path < o[j].Path
}

type Repo struct {
	Name        string
	Description string
}

type RepoModel struct {
	RepoDir string
}

func (m *RepoModel) IsRepo(name string) bool {
	path := filepath.Join(m.RepoDir, name)
	finfo, err := os.Stat(path)
	if err != nil {
		return false
	}

	if !finfo.IsDir() {
		return false
	}

	// This command prints false to stdout if inside a git work tree,
	// but only errors if we're not in a repo at all
	cmd := exec.Command("git", "rev-parse", "--is-inside-git-dir")
	cmd.Dir = path
	err = cmd.Run()
	if err != nil {
		return false
	}

	return true
}

func (m *RepoModel) getRepoDesc(name string) string {
	cmd := exec.Command("git", "rev-parse", "--absolute-git-dir")
	cmd.Dir = filepath.Join(m.RepoDir, name)
	dotGitDirB, err := cmd.Output()
	if err != nil {
		return ""
	}
	dotGitDir := strings.TrimSpace(string(dotGitDirB))

	desc := ""
	descBytes, err := os.ReadFile(filepath.Join(dotGitDir, "description"))
	if err == nil {
		desc = string(descBytes)
		if desc == defaultDesc {
			return ""
		}
	}

	return desc
}

func (m *RepoModel) getTrueName(name string) (string, error) {
	if m.IsRepo(name) {
		return name, nil
	}

	if m.IsRepo(name + ".git") {
		return name + ".git", nil
	}

	return "", ErrNoRepo
}

func (m *RepoModel) GetByName(name string) (Repo, error) {
	var r Repo
	r.Name = name
	if !m.IsRepo(name) {
		if m.IsRepo(name + ".git") {
			name += ".git"
		} else {
			return Repo{}, ErrNoRepo
		}
	}

	r.Description = m.getRepoDesc(name)

	return r, nil
}

func (m *RepoModel) GetAll() ([]Repo, error) {
	entries, err := os.ReadDir(m.RepoDir)
	if err != nil {
		return nil, err
	}

	repos := []Repo{}
	for _, e := range entries {
		name := e.Name()
		if !m.IsRepo(name) {
			if m.IsRepo(name + ".git") {
				name += ".git"
			} else {
				continue
			}
		}

		desc := m.getRepoDesc(name)

		repos = append(repos, Repo{strings.TrimSuffix(e.Name(), ".git"), desc})
	}

	return repos, nil
}

func (m *RepoModel) HasCommits(r Repo) bool {
	var path string
	if m.IsRepo(r.Name + ".git") {
		path = filepath.Join(m.RepoDir, r.Name+".git")
	} else if m.IsRepo(r.Name) {
		path = filepath.Join(m.RepoDir, r.Name)
	} else {
		return false
	}

	cmd := exec.Command("git", "log")
	cmd.Dir = path
	err := cmd.Run()
	if err != nil {
		return false
	}

	return true
}

func (m *RepoModel) ListRepoTree(r Repo, tree string, dir string) ([]string, error) {
	var path string
	if m.IsRepo(r.Name + ".git") {
		path = filepath.Join(m.RepoDir, r.Name+".git")
	} else if m.IsRepo(r.Name) {
		path = filepath.Join(m.RepoDir, r.Name)
	} else {
		return nil, ErrNoRepo
	}

	commitDirPath := ""
	if strings.TrimSpace(tree) == "" {
		commitDirPath += "HEAD"
	}

	if strings.TrimSpace(dir) != "" {
		commitDirPath += ":" + dir
	}

	cmd := exec.Command("git", "ls-tree", "--format=\"%(objecttype):%(path)\"", commitDirPath)
	cmd.Dir = path

	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	lines := strings.Split(string(out), "\n")
	for i, line := range lines {
		parts := strings.Split(line, ":")
		if parts[0] == "tree" {
			lines[i] = parts[1] + "/"
		} else {
			lines[i] = parts[1]
		}
	}

	return lines, nil
}

func (m *RepoModel) GetHeadBranch(repoName string) (string, error) {
	trueName, err := m.getTrueName(repoName)
	if err != nil {
		return "", err
	}

	cmd := exec.Command("git", "symbolic-ref", "HEAD")
	cmd.Dir = filepath.Join(m.RepoDir, trueName)

	out, err := cmd.Output()
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(filepath.Base(string(out))), nil
}

// This should return err on error, I am being lazy by writing it this way
// Will fix when it becomes a problem
func (m *RepoModel) CountRevs(r Repo) int {
	trueName, err := m.getTrueName(r.Name)
	if err != nil {
		return 0
	}

	cmd := exec.Command("git", "rev-list", "--count", "HEAD")
	cmd.Dir = filepath.Join(m.RepoDir, trueName)

	out, err := cmd.Output()
	if err != nil {
		return 0
	}

	count, err := strconv.Atoi(strings.TrimSpace(string(out)))
	if err != nil {
		return 0
	}
	
	return count
}

func (m *RepoModel) GetTreeObjects(repoName string, tree string, path string) ([]GitObject, error) {
	trueName, err := m.getTrueName(repoName)
	if err != nil {
		return []GitObject{}, err
	}

	treePath := tree
	if strings.TrimSpace(tree) == "" {
		treePath += "HEAD"
	}

	if strings.TrimSpace(path) != "" {
		treePath += ":" + path
	}

	cmd := exec.Command("git", "ls-tree", "-t", "--format=%(objecttype):%(path)", treePath)
	cmd.Dir = filepath.Join(m.RepoDir, trueName)

	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	objs := []GitObject{}
	lines := strings.SplitSeq(string(out), "\n")
	for line := range lines {
		if strings.Contains(line, ":") {
			parts := strings.Split(line, ":")
			objs = append(objs, GitObject{ObjectType: parts[0], Path: parts[1]})
		}
	}

	sort.Sort(ObjectSort(objs))

	return objs, nil
}

func (m *RepoModel) GetBlobObjectText(repoName string, tree string, path string) (string, error) {
	trueName, err := m.getTrueName(repoName)
	if err != nil {
		return "", err
	}

	treePath := ""
	tree = strings.TrimSpace(tree)
	if tree == "" {
		treePath += "HEAD"
	} else {
		treePath += tree
	}

	if strings.TrimSpace(path) == "" {
		return "", errors.New("No path provided")
	}
	treePath += ":" + path

	cmd := exec.Command("git", "show", treePath)
	fmt.Println("Command: ", cmd.String())
	cmd.Dir = filepath.Join(m.RepoDir, trueName)

	out, err := cmd.Output()
	if err != nil {
		return "", err
	}

	return string(out), nil
}

func (m *RepoModel) ListRepoHead(r Repo) ([]string, error) {
	return m.ListRepoTree(r, "", "")
}

func (m *RepoModel) CreateRepo(name, description string) error {
	// check if directory already exists
	if _, err := os.Stat(filepath.Join(m.RepoDir, name)); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
	} else {
		return ErrDuplicateRepo
	}

	cmd := exec.Command("git", "init", "--bare", name)
	cmd.Dir = m.RepoDir
	err := cmd.Run()
	if err != nil {
		return err
	}

	dpath := filepath.Join(m.RepoDir, name, "description")
	df, err := os.Create(dpath)
	// TODO: Identify if the error occured here or later and proceed
	// with the knowledge that the repo exists
	if err != nil {
		return err
	}

	_, err = df.WriteString(description)
	if err != nil {
		return err
	}

	return nil
}