package web

import (
	"bytes"
	"html/template"
	"mime"
	"net/http"
	"path/filepath"
	"strings"

	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/formatters/html"
	"github.com/alecthomas/chroma/v2/lexers"
	"github.com/alecthomas/chroma/v2/styles"
	"github.com/charmbracelet/log/v2"
	"github.com/charmbracelet/soft-serve/pkg/config"
	"github.com/charmbracelet/soft-serve/pkg/proto"
	"github.com/gorilla/mux"
)

// BlobData contains data for rendering file content view.
type BlobData struct {
	RepoBaseData
	Ref          string
	Path         string
	Content      string
	RenderedHTML template.HTML
	IsBinary     bool
	IsMarkdown   bool
	ShowSource   bool
	IsCommitHash bool
}

// repoBlob handles file content view.
func repoBlob(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	logger := log.FromContext(ctx)
	cfg := config.FromContext(ctx)
	repo := proto.RepositoryFromContext(ctx)

	if r.URL.Query().Get("raw") == "1" {
		repoBlobRaw(w, r)
		return
	}

	gr, err := openRepository(repo)
	if err != nil {
		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
		renderInternalServerError(w, r)
		return
	}

	vars := mux.Vars(r)
	refAndPath := vars["refAndPath"]
	ref, path := parseRefAndPath(gr, refAndPath)

	refObj, isCommitHash, err := resolveAndBuildRef(gr, ref)
	if err != nil {
		logger.Debug("failed to resolve ref or commit", "repo", repo.Name(), "ref", ref, "err", err)
		renderNotFound(w, r)
		return
	}

	tree, err := gr.Tree(refObj)
	if err != nil {
		logger.Debug("failed to get tree for ref", "repo", repo.Name(), "ref", ref, "err", err)
		renderNotFound(w, r)
		return
	}

	entry, err := tree.TreeEntry(path)
	if err != nil {
		logger.Debug("failed to get tree entry", "repo", repo.Name(), "ref", ref, "path", path, "err", err)
		renderNotFound(w, r)
		return
	}

	if entry.IsTree() {
		renderNotFound(w, r)
		return
	}

	content, err := entry.Contents()
	if err != nil {
		logger.Debug("failed to get file contents", "repo", repo.Name(), "ref", ref, "path", path, "err", err)
		renderInternalServerError(w, r)
		return
	}

	defaultBranch := getDefaultBranch(gr)

	isMarkdown := isMarkdownFile(path, content)
	showSource := r.URL.Query().Get("source") == "1"
	var renderedHTML template.HTML

	if isMarkdown && !isBinaryContent(content) && !showSource {
		renderedHTML, _ = renderMarkdown(content)
	} else if !isBinaryContent(content) {
		renderedHTML = highlightCode(path, content)
	}

	data := BlobData{
		RepoBaseData: RepoBaseData{
			BaseData: BaseData{
				ServerName: cfg.Name,
				ActiveTab:  "tree",
			},
			Repo:          repo,
			DefaultBranch: defaultBranch,
		},
		Ref:          ref,
		Path:         path,
		Content:      string(content),
		RenderedHTML: renderedHTML,
		IsBinary:     isBinaryContent(content),
		IsMarkdown:   isMarkdown,
		ShowSource:   showSource,
		IsCommitHash: isCommitHash,
	}

	renderHTML(w, "blob.html", data)
}

// repoBlobRaw handles raw file download.
func repoBlobRaw(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	logger := log.FromContext(ctx)
	repo := proto.RepositoryFromContext(ctx)

	gr, err := openRepository(repo)
	if err != nil {
		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
		renderInternalServerError(w, r)
		return
	}

	vars := mux.Vars(r)
	refAndPath := vars["refAndPath"]
	ref, path := parseRefAndPath(gr, refAndPath)

	refObj, _, err := resolveAndBuildRef(gr, ref)
	if err != nil {
		logger.Debug("failed to resolve ref or commit", "repo", repo.Name(), "ref", ref, "err", err)
		renderNotFound(w, r)
		return
	}

	tree, err := gr.Tree(refObj)
	if err != nil {
		logger.Debug("failed to get tree for ref", "repo", repo.Name(), "ref", ref, "err", err)
		renderNotFound(w, r)
		return
	}

	entry, err := tree.TreeEntry(path)
	if err != nil {
		logger.Debug("failed to get tree entry", "repo", repo.Name(), "ref", ref, "path", path, "err", err)
		renderNotFound(w, r)
		return
	}

	if entry.IsTree() {
		renderNotFound(w, r)
		return
	}

	content, err := entry.Contents()
	if err != nil {
		logger.Debug("failed to get file contents", "repo", repo.Name(), "ref", ref, "path", path, "err", err)
		renderInternalServerError(w, r)
		return
	}

	ext := filepath.Ext(path)
	var contentType string

	// For files without extensions, prioritize content detection
	if ext == "" {
		contentType = http.DetectContentType(content)
	} else {
		// For files with extensions, try MIME type lookup first
		contentType = mime.TypeByExtension(ext)
		if contentType == "" {
			contentType = http.DetectContentType(content)
		}
	}

	if strings.HasPrefix(contentType, "text/") && !strings.Contains(contentType, "charset") {
		contentType += "; charset=utf-8"
	}

	w.Header().Set("Content-Type", contentType)
	_, _ = w.Write(content)
}

// isBinaryContent detects if file content is binary using a simple heuristic.
// Returns true if content contains null bytes in the first 8KB.
func isBinaryContent(content []byte) bool {
	// Check first 8KB for null bytes (Git's detection method)
	size := len(content)
	if size > 8000 {
		size = 8000
	}
	for i := 0; i < size; i++ {
		if content[i] == 0 {
			return true
		}
	}
	return false
}

// isMarkdownFile checks if a file has a markdown extension or contains markdown content.
func isMarkdownFile(path string, content []byte) bool {
	ext := strings.ToLower(filepath.Ext(path))
	if ext == ".md" || ext == ".markdown" {
		return true
	}

	// For files without extensions, use Chroma's lexer detection
	if ext == "" && len(content) > 0 {
		lexer := lexers.Analyse(string(content))
		if lexer != nil {
			config := lexer.Config()
			name := strings.ToLower(config.Name)
			return name == "markdown" || name == "md"
		}
	}

	return false
}

// highlightCode applies syntax highlighting to code and returns HTML.
func highlightCode(path string, content []byte) template.HTML {
	var lexer chroma.Lexer
	ext := filepath.Ext(path)

	// For files without extensions, prioritize content analysis to detect shebangs
	if ext == "" {
		lexer = lexers.Analyse(string(content))
		if lexer == nil {
			lexer = lexers.Match(path)
		}
	} else {
		// For files with extensions, try filename matching first
		lexer = lexers.Match(path)
		if lexer == nil {
			lexer = lexers.Analyse(string(content))
		}
	}

	if lexer == nil {
		lexer = lexers.Fallback
	}

	style := styles.Get("github")
	if style == nil {
		style = styles.Fallback
	}

	formatter := html.New(
		html.WithClasses(true),
		html.WithLineNumbers(true),
		html.WithLinkableLineNumbers(true, ""),
	)

	iterator, err := lexer.Tokenise(nil, string(content))
	if err != nil {
		return template.HTML("<pre><code>" + template.HTMLEscapeString(string(content)) + "</code></pre>")
	}

	var buf bytes.Buffer
	err = formatter.Format(&buf, style, iterator)
	if err != nil {
		return template.HTML("<pre><code>" + template.HTMLEscapeString(string(content)) + "</code></pre>")
	}

	return template.HTML(buf.String())
}
