package admin

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"osaet/backend/internal/ai"

	"github.com/gin-gonic/gin"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Server struct {
	db           *pgxpool.Pool
	store        *Store
	builder      *Builder
	deepSeek     DeepSeekConfig
	localLLM     LocalLLMConfig
	uploader     *ImageUploader
	slugProvider string
	adminDir     string
	staticDir    string
	ctx          context.Context
}

func NewServer(db *pgxpool.Pool) *Server {
	return NewServerWithConfig(db, Config{})
}

func NewServerWithConfig(db *pgxpool.Pool, cfg Config) *Server {
	return NewServerWithContext(context.Background(), db, cfg)
}

func NewServerWithContext(ctx context.Context, db *pgxpool.Pool, cfg Config) *Server {
	var store *Store
	var builder *Builder
	if db != nil {
		store = NewStore(db)
		if cfg.PostsDir != "" && cfg.SiteDir != "" {
			builder = NewBuilder(store, NewExporter(cfg.PostsDir), cfg.SiteDir)
			builder.Start(ctx)
		}
	}
	var uploader *ImageUploader
	if cfg.R2.Endpoint != "" || cfg.R2.Bucket != "" || cfg.R2.AccessKeyID != "" || cfg.R2.SecretAccessKey != "" {
		createdUploader, err := NewImageUploader(ctx, cfg.R2)
		if err != nil {
			log.Printf("R2 image uploader disabled: %v", err)
		} else {
			uploader = createdUploader
		}
	}

	return &Server{
		db:           db,
		store:        store,
		builder:      builder,
		deepSeek:     cfg.DeepSeek,
		localLLM:     cfg.LocalLLM,
		uploader:     uploader,
		slugProvider: cfg.SlugProvider,
		adminDir:     cfg.AdminDir,
		staticDir:    cfg.StaticDir,
		ctx:          ctx,
	}
}

func (s *Server) Router() http.Handler {
	gin.SetMode(gin.ReleaseMode)

	r := gin.New()
	r.Use(gin.Recovery())
	r.Use(requestLogger())

	r.GET("/healthz", s.health)
	r.GET("/readyz", s.ready)
	r.GET("/admin", s.adminPage)
	r.GET("/admin/", s.adminPage)
	r.GET("/admin/:filepath", s.adminFile)

	api := r.Group("/api/admin")
	api.GET("/health", s.health)
	api.POST("/login", s.login)

	protected := api.Group("")
	protected.Use(s.requireAuth)
	protected.GET("/me", s.me)
	protected.POST("/logout", s.logout)
	protected.POST("/slug", s.generateSlug)
	protected.POST("/images", s.uploadImage)
	protected.GET("/audit-logs", s.listAuditLogs)
	protected.GET("/posts", s.listPosts)
	protected.POST("/posts", s.createPost)
	protected.GET("/posts/:id", s.getPost)
	protected.PUT("/posts/:id", s.updatePost)
	protected.DELETE("/posts/:id", s.deletePost)
	protected.POST("/posts/:id/build", s.buildPost)
	protected.POST("/posts/:id/publish", s.publishPost)
	protected.GET("/build-jobs/:id", s.getBuildJob)

	r.NoRoute(s.siteFile)

	return r
}

func (s *Server) health(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"ok":      true,
		"service": "osaet-admin",
	})
}

func (s *Server) adminPage(c *gin.Context) {
	page, err := adminIndex(s.adminDir)
	if err != nil {
		c.String(http.StatusInternalServerError, err.Error())
		return
	}
	c.Data(http.StatusOK, "text/html; charset=utf-8", page)
}

func (s *Server) adminFile(c *gin.Context) {
	if serveAdminFile(c, s.adminDir) {
		return
	}
	s.adminPage(c)
}

func (s *Server) siteFile(c *gin.Context) {
	if strings.HasPrefix(c.Request.URL.Path, "/api/") {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}
	if serveSiteFile(c, s.staticDir) {
		return
	}
	c.String(http.StatusNotFound, "not found")
}

func (s *Server) ready(c *gin.Context) {
	if s.db == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"ok":    false,
			"error": "database is not configured",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
	defer cancel()

	if err := s.db.Ping(ctx); err != nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"ok":    false,
			"error": err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
}

func (s *Server) listPosts(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	opts := PostListOptions{
		Status: PostStatus(c.Query("status")),
		Limit:  queryInt(c, "limit"),
		Offset: queryInt(c, "offset"),
	}
	posts, err := s.store.ListPosts(c.Request.Context(), opts)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	total, err := s.store.CountPosts(c.Request.Context(), opts.Status)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"posts": posts, "total": total})
}

func queryInt(c *gin.Context, key string) int {
	value := c.Query(key)
	if value == "" {
		return 0
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return 0
	}
	return parsed
}

func (s *Server) login(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	var input LoginInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	result, err := s.store.Login(c.Request.Context(), input)
	if err != nil {
		s.audit(c, nil, "login_failed", "user", "", gin.H{"username": input.Username, "error": err.Error()})
		writeStoreError(c, err)
		return
	}
	SetSessionCookie(c, result.Token, result.ExpiresAt)
	s.audit(c, &result.User, "login", "user", result.User.ID, gin.H{"username": result.User.Username})
	c.JSON(http.StatusOK, gin.H{
		"user":      result.User,
		"expiresAt": result.ExpiresAt,
	})
}

func (s *Server) me(c *gin.Context) {
	user, ok := c.Request.Context().Value(userContextKey).(User)
	if !ok {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"user": user})
}

func (s *Server) logout(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	user, _ := currentUser(c)
	token, _ := c.Cookie(SessionCookieName)
	if err := s.store.Logout(c.Request.Context(), token); err != nil {
		writeStoreError(c, err)
		return
	}
	ClearSessionCookie(c)
	s.audit(c, &user, "logout", "user", user.ID, gin.H{"username": user.Username})
	c.JSON(http.StatusOK, gin.H{"ok": true})
}

func (s *Server) getPost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	post, err := s.store.GetPost(c.Request.Context(), c.Param("id"))
	if err != nil {
		writeStoreError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"post": post})
}

func (s *Server) createPost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	var input PostInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	post, err := s.store.CreatePost(c.Request.Context(), input)
	if err != nil {
		writeStoreError(c, err)
		return
	}
	s.auditCurrentUser(c, "post_create", "post", post.ID, postAuditDetails(post))
	c.JSON(http.StatusCreated, gin.H{"post": post})
}

func (s *Server) updatePost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	var input PostInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	post, err := s.store.UpdatePost(c.Request.Context(), c.Param("id"), input)
	if err != nil {
		writeStoreError(c, err)
		return
	}
	s.auditCurrentUser(c, "post_update", "post", post.ID, postAuditDetails(post))
	c.JSON(http.StatusOK, gin.H{"post": post})
}

func (s *Server) deletePost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	job, err := s.store.DeletePost(c.Request.Context(), c.Param("id"))
	if err != nil {
		writeStoreError(c, err)
		return
	}
	s.enqueueBuildJob(job)
	details := gin.H{}
	if job != nil {
		details["buildJobId"] = job.ID
	}
	s.auditCurrentUser(c, "post_delete", "post", c.Param("id"), details)
	c.JSON(http.StatusOK, gin.H{"ok": true, "buildJob": job})
}

type GenerateSlugInput struct {
	Title   string `json:"title"`
	Summary string `json:"summary"`
	PostID  string `json:"postId"`
}

func (s *Server) generateSlug(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	var input GenerateSlugInput
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if strings.TrimSpace(input.Title) == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
		return
	}
	ctx, cancel := context.WithTimeout(c.Request.Context(), 35*time.Second)
	defer cancel()

	base, err := s.generateSlugBase(ctx, input.Title, input.Summary)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
		return
	}
	slug, err := s.store.UniqueSlug(c.Request.Context(), base, input.PostID)
	if err != nil {
		writeStoreError(c, err)
		return
	}
	s.auditCurrentUser(c, "slug_generate", "post", input.PostID, gin.H{"title": input.Title, "slug": slug})
	c.JSON(http.StatusOK, gin.H{"slug": slug})
}

func (s *Server) uploadImage(c *gin.Context) {
	if s.uploader == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "R2 image uploader is not configured"})
		return
	}

	file, err := c.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
		return
	}
	opened, err := file.Open()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	defer opened.Close()

	uploaded, err := s.uploader.Upload(c.Request.Context(), file.Filename, opened, file.Size)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	s.auditCurrentUser(c, "image_upload", "image", uploaded.Key, gin.H{
		"filename":    uploaded.Filename,
		"url":         uploaded.URL,
		"size":        uploaded.Size,
		"contentType": uploaded.ContentType,
	})
	c.JSON(http.StatusCreated, gin.H{"image": uploaded})
}

func (s *Server) listAuditLogs(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	opts := AuditLogListOptions{
		Action:       c.Query("action"),
		ResourceType: c.Query("resourceType"),
		Query:        c.Query("query"),
		Limit:        queryInt(c, "limit"),
		Offset:       queryInt(c, "offset"),
	}
	logs, err := s.store.ListAuditLogs(c.Request.Context(), opts)
	if err != nil {
		writeStoreError(c, err)
		return
	}
	total, err := s.store.CountAuditLogs(c.Request.Context(), opts)
	if err != nil {
		writeStoreError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"logs": logs, "total": total})
}

func (s *Server) generateSlugBase(ctx context.Context, title string, summary string) (string, error) {
	switch strings.ToLower(strings.TrimSpace(s.slugProvider)) {
	case "", "deepseek":
		apiKey := strings.TrimSpace(s.deepSeek.APIKey)
		if apiKey == "" {
			return "", errors.New("DEEPSEEK_API_KEY is not configured")
		}
		return ai.GenerateSlug(ctx, ai.Config{
			APIKey:  apiKey,
			BaseURL: s.deepSeek.BaseURL,
			Model:   s.deepSeek.Model,
		}, title, summary)
	case "local", "local_llm", "ollama":
		return ai.GenerateLocalSlug(ctx, ai.LocalConfig{
			URL:         s.localLLM.URL,
			Model:       s.localLLM.Model,
			Temperature: s.localLLM.Temperature,
			TopP:        s.localLLM.TopP,
			NumPredict:  s.localLLM.NumPredict,
		}, title, summary)
	default:
		return "", fmt.Errorf("unsupported slug provider %q", s.slugProvider)
	}
}

func (s *Server) buildPost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	job, err := s.store.CreateManualBuildJob(c.Request.Context(), c.Param("id"))
	if err != nil {
		writeStoreError(c, err)
		return
	}

	s.enqueueBuildJob(&job)
	s.auditCurrentUser(c, "build_create", "build_job", job.ID, gin.H{"postId": c.Param("id")})
	c.JSON(http.StatusAccepted, gin.H{"buildJob": job})
}

func (s *Server) publishPost(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	post, job, err := s.store.PublishPost(c.Request.Context(), c.Param("id"))
	if err != nil {
		writeStoreError(c, err)
		return
	}

	s.enqueueBuildJob(&job)
	s.auditCurrentUser(c, "post_publish", "post", post.ID, gin.H{
		"title":      post.Title,
		"slug":       post.Slug,
		"status":     post.Status,
		"buildJobId": job.ID,
	})

	c.JSON(http.StatusAccepted, gin.H{
		"post":     post,
		"buildJob": job,
	})
}

func (s *Server) enqueueBuildJob(job *BuildJob) {
	if job == nil || s.builder == nil {
		return
	}
	if !s.builder.Enqueue(job.ID) {
		_ = s.store.MarkBuildJobFailed(context.Background(), job.ID, "", "build queue is full")
	}
}

func (s *Server) getBuildJob(c *gin.Context) {
	if !s.requireStore(c) {
		return
	}

	job, err := s.store.GetBuildJob(c.Request.Context(), c.Param("id"))
	if err != nil {
		writeStoreError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"buildJob": job})
}

func (s *Server) requireStore(c *gin.Context) bool {
	if s.store != nil {
		return true
	}
	c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database is not configured"})
	return false
}

func (s *Server) requireAuth(c *gin.Context) {
	if !s.requireStore(c) {
		c.Abort()
		return
	}

	token, err := c.Cookie(SessionCookieName)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		c.Abort()
		return
	}

	user, err := s.store.UserBySessionToken(c.Request.Context(), token)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		c.Abort()
		return
	}

	ctx := context.WithValue(c.Request.Context(), userContextKey, user)
	c.Request = c.Request.WithContext(ctx)
	c.Next()
}

func writeStoreError(c *gin.Context, err error) {
	switch err {
	case ErrNotFound:
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
	case ErrInvalidCredentials:
		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
}

func currentUser(c *gin.Context) (User, bool) {
	user, ok := c.Request.Context().Value(userContextKey).(User)
	return user, ok
}

func (s *Server) auditCurrentUser(c *gin.Context, action string, resourceType string, resourceID string, details map[string]any) {
	user, ok := currentUser(c)
	if !ok {
		s.audit(c, nil, action, resourceType, resourceID, details)
		return
	}
	s.audit(c, &user, action, resourceType, resourceID, details)
}

func (s *Server) audit(c *gin.Context, user *User, action string, resourceType string, resourceID string, details map[string]any) {
	if s.store == nil {
		return
	}
	var actorID *string
	actorUsername := ""
	if user != nil {
		actorID = &user.ID
		actorUsername = user.Username
	}
	if err := s.store.CreateAuditLog(c.Request.Context(), AuditLogInput{
		ActorID:       actorID,
		ActorUsername: actorUsername,
		Action:        action,
		ResourceType:  resourceType,
		ResourceID:    resourceID,
		IPAddress:     c.ClientIP(),
		UserAgent:     c.Request.UserAgent(),
		Details:       details,
	}); err != nil {
		log.Printf("audit log failed: %v", err)
	}
}

func requestLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		started := time.Now()
		c.Next()
		log.Printf("%s %s %d %s %s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), c.ClientIP())
	}
}

func postAuditDetails(post Post) map[string]any {
	return map[string]any{
		"title":   post.Title,
		"slug":    post.Slug,
		"status":  post.Status,
		"version": post.Version,
	}
}
