// Package web implements the HTTP server and web UI for Soft Serve.
package web

//go:generate go run gen_syntax_css.go

import (
	"context"
	"embed"
	"fmt"
	"html/template"
	"mime"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"charm.land/log/v2"
	"github.com/charmbracelet/soft-serve/git"
	"github.com/charmbracelet/soft-serve/pkg/proto"
	"github.com/dustin/go-humanize"
	"github.com/gorilla/mux"
)

//go:embed templates/*.html
var templatesFS embed.FS

//go:embed static/*
var staticFS embed.FS

const (
	// defaultCommitsPerPage is the number of commits shown per page in commit history view.
	defaultCommitsPerPage = 50
	// defaultReposPerPage is the number of repositories shown per page on the home page.
	defaultReposPerPage = 20
	// defaultTagsPerPage is the number of tags shown per page on the tags page.
	defaultTagsPerPage = 20
	// defaultBranchesPerPage is the number of branches shown per page on the branches page.
	defaultBranchesPerPage = 20
)

// BaseData contains common fields for all web UI pages.
type BaseData struct {
	ServerName  string
	ActiveTab   string
	Title       string
	Description string
}

// RepoBaseData contains common fields for repository-specific pages.
type RepoBaseData struct {
	BaseData
	Repo          proto.Repository
	DefaultBranch string
	HasGitBug     bool
}

// PaginationData contains common fields for paginated views.
type PaginationData struct {
	Page        int
	TotalPages  int
	HasPrevPage bool
	HasNextPage bool
}

// templateFuncs defines template helper functions available in HTML templates.
// Functions include: splitPath (split path into components), joinPath (join path components),
// parentPath (get parent directory), shortHash (truncate commit hash), formatDate (format timestamp),
// and humanizeSize (format file size in human-readable format).
var templateFuncs = template.FuncMap{
	"sub": func(a, b int) int {
		return a - b
	},
	"splitPath": func(path string) []string {
		if path == "." {
			return []string{}
		}
		return strings.Split(path, "/")
	},
	"joinPath": func(index int, data interface{}) string {
		var path string

		switch v := data.(type) {
		case BlobData:
			path = v.Path
		case TreeData:
			path = v.Path
		default:
			return ""
		}

		parts := strings.Split(path, "/")
		if index >= len(parts) {
			return path
		}
		return strings.Join(parts[:index+1], "/")
	},
	"parentPath": func(path string) string {
		if path == "." || !strings.Contains(path, "/") {
			return "."
		}
		return filepath.Dir(path)
	},
	"shortHash": func(hash interface{}) string {
		var hashStr string
		switch v := hash.(type) {
		case string:
			hashStr = v
		case fmt.Stringer:
			hashStr = v.String()
		default:
			hashStr = fmt.Sprintf("%v", hash)
		}
		if len(hashStr) > 7 {
			return hashStr[:7]
		}
		return hashStr
	},
	"formatDate": func(t interface{}) string {
		switch v := t.(type) {
		case time.Time:
			return v.Format("2006-01-02 15:04:05 UTC")
		default:
			if time, ok := t.(fmt.Stringer); ok {
				return time.String()
			}
			return fmt.Sprintf("%v", t)
		}
	},
	"humanizeSize": func(size int64) string {
		const unit = 1024
		if size < unit {
			return fmt.Sprintf("%d B", size)
		}
		div, exp := int64(unit), 0
		for n := size / unit; n >= unit; n /= unit {
			div *= unit
			exp++
		}
		return fmt.Sprintf("%.1f %ciB", float64(size)/float64(div), "KMGTPE"[exp])
	},
	"inc": func(i int) int {
		return i + 1
	},
	"dec": func(i int) int {
		return i - 1
	},
	"commitSubject": func(message string) string {
		lines := strings.Split(message, "\n")
		if len(lines) > 0 {
			return lines[0]
		}
		return message
	},
	"commitBody": func(message string) string {
		lines := strings.Split(message, "\n")
		if len(lines) <= 1 {
			return ""
		}
		// Skip the subject line and join the rest
		body := strings.Join(lines[1:], "\n")
		return strings.TrimSpace(body)
	},
	"rfc3339": func(t interface{}) string {
		switch v := t.(type) {
		case time.Time:
			return v.Format(time.RFC3339)
		default:
			return ""
		}
	},
	"relativeTime": func(t interface{}) string {
		switch v := t.(type) {
		case time.Time:
			return humanize.Time(v)
		default:
			return ""
		}
	},
	"formatAttribution": func(commit interface{}) string {
		var authorName, message string

		switch v := commit.(type) {
		case *git.Commit:
			if v == nil {
				return ""
			}
			authorName = v.Author.Name
			message = v.Message
		default:
			return ""
		}

		coAuthors := GetCoAuthors(message)

		if len(coAuthors) == 0 {
			return authorName
		}

		if len(coAuthors) == 1 {
			return authorName + " and " + coAuthors[0]
		}

		// Multiple co-authors: "Author, Co1, Co2, and Co3"
		result := authorName
		for i, coAuthor := range coAuthors {
			if i == len(coAuthors)-1 {
				result += ", and " + coAuthor
			} else {
				result += ", " + coAuthor
			}
		}
		return result
	},
	"attributionNames": func(commit interface{}) []string {
		switch v := commit.(type) {
		case *git.Commit:
			if v == nil {
				return nil
			}
			names := []string{v.Author.Name}
			for _, n := range GetCoAuthors(v.Message) {
				n = strings.TrimSpace(n)
				if n != "" {
					names = append(names, n)
				}
			}
			return names
		default:
			return nil
		}
	},
}

// renderHTML renders an HTML template with the given data.
func renderHTML(w http.ResponseWriter, templateName string, data interface{}) {
	tmpl, err := template.New("").Funcs(templateFuncs).ParseFS(templatesFS, "templates/base.html", "templates/"+templateName)
	if err != nil {
		log.Debug("failed to parse template", "template", templateName, "err", err)
		renderInternalServerError(w, nil)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Security headers
	// Note: style-src 'unsafe-inline' is required for inline styles in templates (tree.html, overview.html)
	w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' https: data:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; frame-ancestors 'self'; base-uri 'none'")
	w.Header().Set("Referrer-Policy", "no-referrer")
	w.Header().Set("X-Content-Type-Options", "nosniff")

	if err := tmpl.ExecuteTemplate(w, "layout", data); err != nil {
		log.Debug("template execution failed", "template", templateName, "err", err)
		// Already started writing response, so we can't render an error page
	}
}

// staticFiles handles static file serving.
func staticFiles(w http.ResponseWriter, r *http.Request) {
	// Strip /static/ prefix
	path := strings.TrimPrefix(r.URL.Path, "/static/")

	data, err := staticFS.ReadFile("static/" + path)
	if err != nil {
		renderNotFound(w, r)
		return
	}

	// Set cache headers
	w.Header().Set("Cache-Control", "public, max-age=31536000")

	// Detect content type from extension
	contentType := mime.TypeByExtension(filepath.Ext(path))
	if contentType != "" {
		w.Header().Set("Content-Type", contentType)
	}

	w.Write(data)
}

// withWebUIAccess wraps withAccess and hides 401/403 as 404 for Web UI routes.
func withWebUIAccess(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		logger := log.FromContext(ctx)
		logger.Debug("withWebUIAccess called", "path", r.URL.Path)

		// Wrap the writer to suppress 401/403
		hrw := &hideAuthWriter{ResponseWriter: w}
		// Run access first so repo/user are injected into context
		withAccess(next).ServeHTTP(hrw, r)

		if hrw.suppressed {
			logger.Debug("suppressed 401/403, rendering 404")
			// Remove auth challenge headers so we don't leak private info
			w.Header().Del("WWW-Authenticate")
			w.Header().Del("LFS-Authenticate")
			// Now render 404 once; no double WriteHeader occurs
			renderNotFound(w, r)
		}
	})
}

// hideAuthWriter suppresses 401/403 responses to convert them to 404.
type hideAuthWriter struct {
	http.ResponseWriter
	suppressed bool
}

func (w *hideAuthWriter) WriteHeader(code int) {
	if code == http.StatusUnauthorized || code == http.StatusForbidden {
		// Suppress original status/body; we'll render 404 afterwards
		w.suppressed = true
		return
	}
	w.ResponseWriter.WriteHeader(code)
}

func (w *hideAuthWriter) Write(p []byte) (int, error) {
	if w.suppressed {
		// Drop body of 401/403
		return len(p), nil
	}
	return w.ResponseWriter.Write(p)
}

// WebUIController registers HTTP routes for the web-based repository browser.
// It provides HTML views for repository overview, file browsing, commits, and references.
func WebUIController(ctx context.Context, r *mux.Router) {
	basePrefix := "/{repo:.*}"

	// Static files (most specific, should be first)
	r.PathPrefix("/static/").HandlerFunc(staticFiles).Methods(http.MethodGet)

	// Home page (root path, before other routes)
	r.HandleFunc("/", home).Methods(http.MethodGet)

	// About page
	r.HandleFunc("/about", about).Methods(http.MethodGet)

	// More specific routes must be registered before catch-all patterns
	// Middleware order: withRepoVars (set vars) -> withWebUIAccess (auth + load context) -> handler
	// Tree routes - use catch-all pattern and parse ref/path in handler to support refs with slashes
	r.Handle(basePrefix+"/tree/{refAndPath:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTree)))).
		Methods(http.MethodGet)
	r.Handle(basePrefix+"/tree", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTree)))).
		Methods(http.MethodGet)

	// Blob routes - use catch-all pattern and parse ref/path in handler to support refs with slashes
	r.Handle(basePrefix+"/blob/{refAndPath:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBlob)))).
		Methods(http.MethodGet)

	// Commits routes - use catch-all pattern to support refs with slashes
	r.Handle(basePrefix+"/commits/{ref:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommits)))).
		Methods(http.MethodGet)
	r.Handle(basePrefix+"/commits", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommits)))).
		Methods(http.MethodGet)

	// Commit route
	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommit)))).
		Methods(http.MethodGet)

	// Commit patch and diff routes
	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}.patch", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommitPatch)))).
		Methods(http.MethodGet)
	r.Handle(basePrefix+"/commit/{hash:[0-9a-f]+}.diff", withRepoVars(withWebUIAccess(http.HandlerFunc(repoCommitDiff)))).
		Methods(http.MethodGet)

	// Branches route
	r.Handle(basePrefix+"/branches", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBranches)))).
		Methods(http.MethodGet)

	// Tags route
	r.Handle(basePrefix+"/tags", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTags)))).
		Methods(http.MethodGet)

	// Tag detail route
	r.Handle(basePrefix+"/tag/{tag:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTag)))).
		Methods(http.MethodGet)


	// Bugs routes
	r.Handle(basePrefix+"/bugs", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBugs)))).
		Methods(http.MethodGet)
	r.Handle(basePrefix+"/bug/{hash:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBug)))).
		Methods(http.MethodGet)

	// Repository overview (catch-all, must be last)
	r.Handle(basePrefix, withRepoVars(withWebUIAccess(http.HandlerFunc(repoOverview)))).
		Methods(http.MethodGet)
}
