package web

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"git-browser/internal/repository"

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

const commitsPerPage = 100

type Server struct {
	store     *repository.Store
	templates *template.Template
	config    Config
}

type Config struct {
	SSHUser   string
	CloneHost string
	CloneRoot string
}

type breadcrumb struct {
	Name string
	URL  string
}

type treeEntryView struct {
	Name  string
	URL   string
	IsDir bool
}

type branchView struct {
	Name    string
	URL     string
	Hash    string
	Current bool
}

type commitView struct {
	Hash    string
	URL     string
	Author  string
	Date    string
	Subject string
}

type pageData struct {
	Title string
}

type indexData struct {
	pageData
	Repos []repoLink
}

type repoLink struct {
	Name string
	URL  string
}

type repoData struct {
	pageData
	RepoName      string
	HomeURL       string
	CloneURL      string
	Revision      string
	RevisionLabel string
	Empty         bool
	TreeURL       string
	LogURL        string
	BranchesURL   string
	Breadcrumbs   []breadcrumb
	Entries       []treeEntryView
	ReadmeName    string
	Readme        string
}

type blobData struct {
	pageData
	RepoName      string
	HomeURL       string
	CloneURL      string
	Revision      string
	RevisionLabel string
	Empty         bool
	TreeURL       string
	LogURL        string
	BranchesURL   string
	Breadcrumbs   []breadcrumb
	FilePath      string
	DirectoryURL  string
	HistoryURL    string
	RawURL        string
	Content       string
	ContentType   string
}

type logData struct {
	pageData
	RepoName      string
	HomeURL       string
	CloneURL      string
	Revision      string
	RevisionLabel string
	Empty         bool
	TreeURL       string
	LogURL        string
	BranchesURL   string
	Path          string
	BackURL       string
	Commits       []commitView
	PreviousURL   string
	NextURL       string
	Page          int
}

type branchesData struct {
	pageData
	RepoName      string
	HomeURL       string
	CloneURL      string
	Revision      string
	RevisionLabel string
	Empty         bool
	TreeURL       string
	LogURL        string
	BranchesURL   string
	Branches      []branchView
}

func NewServer(store *repository.Store, config Config) (*Server, error) {
	tmpl, err := parseTemplates()
	if err != nil {
		return nil, err
	}

	return &Server{store: store, templates: tmpl, config: config}, nil
}

func (s *Server) Handler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/static/style.css", s.serveStyle)
	mux.HandleFunc("/static/copy.js", s.serveCopyScript)
	mux.HandleFunc("/", s.route)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !strings.HasPrefix(r.URL.Path, "/static/") {
			s.store.RefreshAsync(func(err error) {
				log.Printf("refresh repositories: %v", err)
			})
		}
		mux.ServeHTTP(w, r)
	})
}

func (s *Server) route(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		s.index(w, r)
		return
	}

	repoName, rest := shiftPath(r.URL.Path)
	if repoName == "" {
		http.NotFound(w, r)
		return
	}

	if rest == "" || rest == "/" {
		s.repoHome(w, r, repoName)
		return
	}

	section, tail := shiftPath(rest)
	switch section {
	case "tree":
		s.tree(w, r, repoName, tail)
	case "blob":
		s.blob(w, r, repoName, tail)
	case "log":
		s.log(w, r, repoName, tail)
	case "branches":
		s.branches(w, r, repoName)
	default:
		http.NotFound(w, r)
	}
}

func (s *Server) index(w http.ResponseWriter, r *http.Request) {
	repos := s.store.List()
	items := make([]repoLink, 0, len(repos))
	for _, repo := range repos {
		items = append(items, repoLink{
			Name: displayRepoName(repo.Name),
			URL:  homeURL(repo.Name),
		})
	}

	data := indexData{
		pageData: pageData{Title: "Repositories"},
		Repos:    items,
	}
	s.render(w, "index.html", data)
}

func (s *Server) repoHome(w http.ResponseWriter, r *http.Request, repoName string) {
	repo, err := s.store.Open(repoName)
	if err != nil {
		s.writeRepoError(w, r, err)
		return
	}

	rev, err := repo.DefaultRevision()
	if err != nil {
		if errors.Is(err, repository.ErrEmptyRepository) {
			s.renderEmptyRepoPage(w, r, repo)
			return
		}
		s.writeError(w, r, http.StatusInternalServerError, err)
		return
	}

	s.renderTreePage(w, r, repo, rev, "")
}

func (s *Server) renderEmptyRepoPage(w http.ResponseWriter, r *http.Request, repo *repository.Repository) {
	displayName := displayRepoName(repo.Name())
	data := repoData{
		pageData:    pageData{Title: displayName},
		RepoName:    displayName,
		HomeURL:     homeURL(repo.Name()),
		CloneURL:    s.cloneURL(r.Host, repo.Name()),
		Empty:       true,
		BranchesURL: branchesURL(repo.Name(), ""),
	}

	s.render(w, "repo.html", data)
}

func (s *Server) tree(w http.ResponseWriter, r *http.Request, repoName, tail string) {
	repo, err := s.store.Open(repoName)
	if err != nil {
		s.writeRepoError(w, r, err)
		return
	}

	rev, treePath, err := splitRevisionAndPath(repo, tail)
	if err != nil {
		s.writeError(w, r, http.StatusNotFound, err)
		return
	}

	s.renderTreePage(w, r, repo, rev, treePath)
}

func (s *Server) renderTreePage(w http.ResponseWriter, r *http.Request, repo *repository.Repository, rev *repository.ResolvedRevision, treePath string) {
	tree, err := repo.Tree(rev.Commit, treePath)
	if err != nil {
		status := http.StatusInternalServerError
		if errors.Is(err, repository.ErrNotTree) {
			status = http.StatusNotFound
		}
		s.writeError(w, r, status, err)
		return
	}

	entries := repo.TreeEntries(tree, treePath)
	readme, err := repo.Readme(tree)
	if err != nil {
		s.writeError(w, r, http.StatusInternalServerError, err)
		return
	}

	entryViews := make([]treeEntryView, 0, len(entries))
	for _, entry := range entries {
		target := "blob"
		if entry.IsDir {
			target = "tree"
		}
		entryViews = append(entryViews, treeEntryView{
			Name:  entry.Name,
			URL:   objectURL(repo.Name(), target, rev.Name, entry.Path),
			IsDir: entry.IsDir,
		})
	}

	displayName := displayRepoName(repo.Name())
	data := repoData{
		pageData:      pageData{Title: displayName},
		RepoName:      displayName,
		HomeURL:       homeURL(repo.Name()),
		CloneURL:      s.cloneURL(r.Host, repo.Name()),
		Revision:      rev.Name,
		RevisionLabel: displayRevision(rev),
		TreeURL:       objectURL(repo.Name(), "tree", rev.Name, treePath),
		LogURL:        repoURL(repo.Name(), "log", rev.Name),
		BranchesURL:   branchesURL(repo.Name(), rev.Name),
		Entries:       entryViews,
	}

	if readme != nil {
		data.ReadmeName = readme.Name
		data.Readme = readme.Content
	}
	if treePath != "" {
		data.Breadcrumbs = breadcrumbs(repo.Name(), rev.Name, treePath)
	}

	s.render(w, "repo.html", data)
}

func (s *Server) blob(w http.ResponseWriter, r *http.Request, repoName, tail string) {
	repo, err := s.store.Open(repoName)
	if err != nil {
		s.writeRepoError(w, r, err)
		return
	}

	rev, filePath, err := splitRevisionAndPath(repo, tail)
	if err != nil || filePath == "" {
		s.writeError(w, r, http.StatusNotFound, errors.New("file not found"))
		return
	}

	blob, err := repo.Blob(rev.Commit, filePath)
	if err != nil {
		status := http.StatusInternalServerError
		if errors.Is(err, repository.ErrNotFile) {
			status = http.StatusNotFound
		}
		s.writeError(w, r, status, err)
		return
	}

	if r.URL.Query().Get("raw") != "" || !blob.Text {
		s.serveRawBlob(w, r, blob)
		return
	}

	dirPath := path.Dir(filePath)
	if dirPath == "." {
		dirPath = ""
	}

	displayName := displayRepoName(repo.Name())
	data := blobData{
		pageData:      pageData{Title: blob.Name},
		RepoName:      displayName,
		HomeURL:       homeURL(repo.Name()),
		CloneURL:      s.cloneURL(r.Host, repo.Name()),
		Revision:      rev.Name,
		RevisionLabel: displayRevision(rev),
		TreeURL:       objectURL(repo.Name(), "tree", rev.Name, dirPath),
		LogURL:        repoURL(repo.Name(), "log", rev.Name),
		BranchesURL:   branchesURL(repo.Name(), rev.Name),
		FilePath:      filePath,
		DirectoryURL:  objectURL(repo.Name(), "tree", rev.Name, dirPath),
		HistoryURL:    logURL(repo.Name(), rev.Name, filePath, 0),
		RawURL:        objectURL(repo.Name(), "blob", rev.Name, filePath) + "?raw=1",
		Content:       string(blob.Content),
		ContentType:   blob.ContentType,
	}
	data.Breadcrumbs = blobBreadcrumbs(repo.Name(), rev.Name, filePath)

	s.render(w, "blob.html", data)
}

func (s *Server) serveRawBlob(w http.ResponseWriter, r *http.Request, blob *repository.Blob) {
	w.Header().Set("Content-Type", blob.ContentType)
	w.Header().Set("Content-Length", strconv.Itoa(len(blob.Content)))
	http.ServeContent(w, r, blob.Name, time.Time{}, bytes.NewReader(blob.Content))
}

func (s *Server) log(w http.ResponseWriter, r *http.Request, repoName, tail string) {
	repo, err := s.store.Open(repoName)
	if err != nil {
		s.writeRepoError(w, r, err)
		return
	}

	revision := strings.Trim(strings.TrimSpace(tail), "/")
	if revision == "" {
		s.writeError(w, r, http.StatusNotFound, errors.New("revision not found"))
		return
	}

	rev, err := repo.ResolveRevision(revision)
	if err != nil {
		s.writeError(w, r, http.StatusNotFound, err)
		return
	}

	page := 0
	if raw := r.URL.Query().Get("page"); raw != "" {
		value, err := strconv.Atoi(raw)
		if err != nil || value < 0 {
			s.writeError(w, r, http.StatusBadRequest, errors.New("invalid page"))
			return
		}
		page = value
	}

	filePath := strings.TrimSpace(r.URL.Query().Get("path"))
	result, err := repo.Log(rev.Commit, filePath, page, commitsPerPage)
	if err != nil {
		s.writeError(w, r, http.StatusInternalServerError, err)
		return
	}

	items := make([]commitView, 0, len(result.Commits))
	for _, commit := range result.Commits {
		subject := commit.Message
		if i := strings.IndexByte(subject, '\n'); i >= 0 {
			subject = subject[:i]
		}

		targetURL := objectURL(repo.Name(), "tree", commit.Hash.String(), "")
		if filePath != "" {
			targetURL = objectURL(repo.Name(), "blob", commit.Hash.String(), filePath)
		}
		items = append(items, commitView{
			Hash:    shortHash(commit.Hash.String()),
			URL:     targetURL,
			Author:  commit.Author.Name,
			Date:    commit.Author.When.Format("2006-01-02 15:04"),
			Subject: subject,
		})
	}

	displayName := displayRepoName(repo.Name())
	data := logData{
		pageData:      pageData{Title: fmt.Sprintf("%s log", displayName)},
		RepoName:      displayName,
		HomeURL:       homeURL(repo.Name()),
		CloneURL:      s.cloneURL(r.Host, repo.Name()),
		Revision:      rev.Name,
		RevisionLabel: displayRevision(rev),
		TreeURL:       objectURL(repo.Name(), "tree", rev.Name, ""),
		LogURL:        repoURL(repo.Name(), "log", rev.Name),
		BranchesURL:   branchesURL(repo.Name(), rev.Name),
		Path:          filePath,
		Commits:       items,
		Page:          page,
	}
	if filePath != "" {
		data.BackURL = objectURL(repo.Name(), "blob", rev.Name, filePath)
	}
	if page > 0 {
		data.PreviousURL = logURL(repo.Name(), rev.Name, filePath, page-1)
	}
	if result.HasMore {
		data.NextURL = logURL(repo.Name(), rev.Name, filePath, page+1)
	}

	s.render(w, "log.html", data)
}

func (s *Server) branches(w http.ResponseWriter, r *http.Request, repoName string) {
	repo, err := s.store.Open(repoName)
	if err != nil {
		s.writeRepoError(w, r, err)
		return
	}

	var rev *repository.ResolvedRevision
	selectedRevision := strings.TrimSpace(r.URL.Query().Get("rev"))
	if selectedRevision != "" {
		rev, err = repo.ResolveRevision(selectedRevision)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, repository.ErrEmptyRepository) || errors.Is(err, plumbing.ErrReferenceNotFound) {
				status = http.StatusNotFound
			}
			s.writeError(w, r, status, err)
			return
		}
	} else {
		rev, err = repo.DefaultRevision()
		if err != nil {
			if !errors.Is(err, repository.ErrEmptyRepository) {
				s.writeError(w, r, http.StatusInternalServerError, err)
				return
			}
		}
	}

	branches, err := repo.Branches()
	if err != nil {
		s.writeError(w, r, http.StatusInternalServerError, err)
		return
	}

	items := make([]branchView, 0, len(branches))
	for _, branch := range branches {
		items = append(items, branchView{
			Name:    branch.Name,
			URL:     objectURL(repo.Name(), "tree", branch.Name, ""),
			Hash:    shortHash(branch.Hash.String()),
			Current: branch.Current,
		})
	}

	displayName := displayRepoName(repo.Name())
	data := branchesData{
		pageData:    pageData{Title: fmt.Sprintf("%s branches", displayName)},
		RepoName:    displayName,
		HomeURL:     homeURL(repo.Name()),
		CloneURL:    s.cloneURL(r.Host, repo.Name()),
		Empty:       rev == nil,
		BranchesURL: branchesURL(repo.Name(), selectedRevision),
		Branches:    items,
	}
	if rev != nil {
		data.Revision = rev.Name
		data.RevisionLabel = displayRevision(rev)
		data.TreeURL = objectURL(repo.Name(), "tree", rev.Name, "")
		data.LogURL = repoURL(repo.Name(), "log", rev.Name)
		data.BranchesURL = branchesURL(repo.Name(), rev.Name)
	}

	s.render(w, "branches.html", data)
}

func (s *Server) serveStyle(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/css; charset=utf-8")
	http.ServeFileFS(w, r, assets, "static/style.css")
}

func (s *Server) serveCopyScript(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
	http.ServeFileFS(w, r, assets, "static/copy.js")
}

func (s *Server) render(w http.ResponseWriter, name string, data any) {
	var body bytes.Buffer
	if err := s.templates.ExecuteTemplate(&body, name, data); err != nil {
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write(body.Bytes())
}

func (s *Server) writeRepoError(w http.ResponseWriter, r *http.Request, err error) {
	status := http.StatusInternalServerError
	if errors.Is(err, os.ErrNotExist) {
		status = http.StatusNotFound
	}
	s.writeError(w, r, status, err)
}

func (s *Server) writeError(w http.ResponseWriter, r *http.Request, status int, err error) {
	http.Error(w, http.StatusText(status), status)
}

func splitRevisionAndPath(repo *repository.Repository, raw string) (*repository.ResolvedRevision, string, error) {
	trimmed := strings.Trim(strings.TrimSpace(raw), "/")
	if trimmed == "" {
		return nil, "", errors.New("revision not found")
	}

	parts := strings.Split(trimmed, "/")
	for i := len(parts); i >= 1; i-- {
		candidate := strings.Join(parts[:i], "/")
		rev, err := repo.ResolveRevision(candidate)
		if err != nil {
			continue
		}
		return rev, strings.Join(parts[i:], "/"), nil
	}

	return nil, "", errors.New("revision not found")
}

func shiftPath(raw string) (string, string) {
	clean := strings.TrimPrefix(raw, "/")
	if clean == "" {
		return "", ""
	}
	parts := strings.SplitN(clean, "/", 2)
	if len(parts) == 1 {
		return parts[0], ""
	}
	return parts[0], "/" + parts[1]
}

func breadcrumbs(repoName, revision, treePath string) []breadcrumb {
	parts := strings.Split(treePath, "/")
	items := make([]breadcrumb, 0, len(parts)+1)
	items = append(items, breadcrumb{
		Name: "root",
		URL:  objectURL(repoName, "tree", revision, ""),
	})

	current := ""
	for _, part := range parts {
		current = path.Join(current, part)
		items = append(items, breadcrumb{
			Name: part,
			URL:  objectURL(repoName, "tree", revision, current),
		})
	}

	return items
}

func blobBreadcrumbs(repoName, revision, filePath string) []breadcrumb {
	parts := strings.Split(filePath, "/")
	items := make([]breadcrumb, 0, len(parts)+1)
	items = append(items, breadcrumb{
		Name: "root",
		URL:  objectURL(repoName, "tree", revision, ""),
	})

	current := ""
	for i, part := range parts {
		current = path.Join(current, part)
		target := objectURL(repoName, "tree", revision, current)
		if i == len(parts)-1 {
			target = objectURL(repoName, "blob", revision, current)
		}
		items = append(items, breadcrumb{
			Name: part,
			URL:  target,
		})
	}

	return items
}

func repoURL(repoName, section, revision string) string {
	base := homeURL(repoName)
	if section == "" {
		return base
	}
	return base + "/" + section + "/" + escapeSlashed(revision)
}

func repoSectionURL(repoName, section string) string {
	return homeURL(repoName) + "/" + section
}

func branchesURL(repoName, revision string) string {
	base := repoSectionURL(repoName, "branches")
	if revision == "" {
		return base
	}
	return base + "?rev=" + url.QueryEscape(revision)
}

func homeURL(repoName string) string {
	return "/" + url.PathEscape(repoName)
}

func objectURL(repoName, section, revision, objectPath string) string {
	base := repoURL(repoName, section, revision)
	if objectPath == "" {
		if section == "tree" {
			return base + "/"
		}
		return base
	}
	return base + "/" + escapeSlashed(objectPath)
}

func logURL(repoName, revision, filePath string, page int) string {
	values := url.Values{}
	if filePath != "" {
		values.Set("path", filePath)
	}
	if page > 0 {
		values.Set("page", strconv.Itoa(page))
	}

	base := repoURL(repoName, "log", revision)
	query := values.Encode()
	if query == "" {
		return base
	}
	return base + "?" + query
}

func (s *Server) cloneURL(host, repoName string) string {
	base := repoName
	if s.config.CloneRoot != "" {
		base = path.Join(s.config.CloneRoot, repoName)
	}
	if s.config.CloneHost != "" {
		host = s.config.CloneHost
	}
	return fmt.Sprintf("%s@%s:%s", s.config.SSHUser, sshHost(host), base)
}

func sshHost(host string) string {
	if parsed, _, err := net.SplitHostPort(host); err == nil {
		host = parsed
	}
	return strings.Trim(host, "[]")
}

func escapeSlashed(value string) string {
	parts := strings.Split(value, "/")
	for i, part := range parts {
		parts[i] = url.PathEscape(part)
	}
	return strings.Join(parts, "/")
}

func shortHash(hash string) string {
	if len(hash) <= 7 {
		return hash
	}
	return hash[:7]
}

func displayRepoName(name string) string {
	if strings.HasSuffix(name, ".git") && len(name) > len(".git") {
		return strings.TrimSuffix(name, ".git")
	}
	return name
}

func displayRevision(rev *repository.ResolvedRevision) string {
	if rev == nil {
		return ""
	}
	if isLikelyHash(rev.Name) {
		return shortHash(rev.Hash.String())
	}
	return rev.Name
}

func isLikelyHash(value string) bool {
	if len(value) < 7 || len(value) > 40 {
		return false
	}
	for _, r := range value {
		if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') {
			return false
		}
	}
	return true
}