package api

import (
	"database/sql"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/rycroftapparel/workpulse-api/internal/httpapi"
	"github.com/rycroftapparel/workpulse-api/internal/permissions"
)

// getMe returns the authenticated user (GET /auth/me and GET /me).
// Inactive users receive 403 forbidden per spec.
func (s *Server) getMe(c *gin.Context) {
	uid := userID(c)
	ctx, cancel := s.ctx(c)
	defer cancel()

	var email, name, role string
	var isActive bool
	var createdAt interface{}
	var avatar sql.NullString
	var passwordHash sql.NullString
	var permRaw []byte
	err := s.DB.QueryRowContext(ctx,
		`SELECT email, name, avatar_url, role, is_active, created_at, password_hash, permissions FROM users WHERE id = $1`, uid,
	).Scan(&email, &name, &avatar, &role, &isActive, &createdAt, &passwordHash, &permRaw)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if !isActive {
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "account is disabled"))
		return
	}

	hasPassword := passwordHash.Valid && strings.TrimSpace(passwordHash.String) != ""
	perms := permissions.Normalize(role, permRaw)
	out := gin.H{
		"id":          uid,
		"email":       email,
		"name":        name,
		"role":        role,
		"isActive":    isActive,
		"createdAt":   createdAt,
		"hasPassword": hasPassword,
		"permissions": permissionsForResponse(perms),
	}
	if avatar.Valid {
		out["avatarUrl"] = avatar.String
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

type patchMeBody struct {
	Name      *string `json:"name"`
	AvatarURL *string `json:"avatarUrl"`
}

// patchMe updates the authenticated user (PATCH /me). Email cannot be changed here.
func (s *Server) patchMe(c *gin.Context) {
	var body patchMeBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", httpapi.BindErrorMessage(err)))
		return
	}
	if body.Name == nil && body.AvatarURL == nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "no fields to update"))
		return
	}
	uid := userID(c)
	ctx, cancel := s.ctx(c)
	defer cancel()

	var isActive bool
	err := s.DB.QueryRowContext(ctx, `SELECT is_active FROM users WHERE id = $1`, uid).Scan(&isActive)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if !isActive {
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "account is disabled"))
		return
	}

	if body.Name != nil {
		if _, err := s.DB.ExecContext(ctx, `UPDATE users SET name = $1 WHERE id = $2`, strings.TrimSpace(*body.Name), uid); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	}
	if body.AvatarURL != nil {
		if _, err := s.DB.ExecContext(ctx, `UPDATE users SET avatar_url = $1 WHERE id = $2`, strings.TrimSpace(*body.AvatarURL), uid); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	}
	if body.Name != nil {
		s.recordUserActivity(c, ctx, uid, "profile.name_update", "Profile updated", "Display name changed", nil)
	}
	s.getMe(c)
}

// meReportsPeriodWhere builds a SQL boolean on reports.created_at for the given table alias (e.g. "r", "reports").
func meReportsPeriodWhere(alias, period string) string {
	col := alias + ".created_at"
	switch strings.ToLower(strings.TrimSpace(period)) {
	case "week":
		return col + ` >= NOW() - INTERVAL '7 days'`
	case "all", "":
		return `TRUE`
	default:
		return col + ` >= date_trunc('month', CURRENT_TIMESTAMP)`
	}
}

// getMeSummary aggregates report and light activity stats for the profile dashboard.
func (s *Server) getMeSummary(c *gin.Context) {
	uid := userID(c)
	period := c.DefaultQuery("period", "month")
	ctx, cancel := s.ctx(c)
	defer cancel()

	var isActive bool
	if err := s.DB.QueryRowContext(ctx, `SELECT is_active FROM users WHERE id = $1`, uid).Scan(&isActive); err != nil || !isActive {
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "account is disabled"))
		return
	}

	scope, err := s.divisionScopeForUser(ctx, uid, roleFromContext(c))
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	where := meReportsPeriodWhere("r", period)
	qReports := `SELECT 
		COUNT(*) FILTER (WHERE r.status = 'draft') AS draft,
		COUNT(*) FILTER (WHERE r.status <> 'draft') AS submitted,
		COUNT(*) AS total
		FROM reports r WHERE (` + where + `)`
	reportArgs := []interface{}{}
	if scope.ViewOrg {
		divClause, divArgs, _ := reportsDivisionSQL("r", scope, 1)
		qReports += divClause
		reportArgs = divArgs
	} else {
		qReports += ` AND r.user_id = $1`
		reportArgs = []interface{}{uid}
	}

	var draft, submitted, total int
	if err := s.DB.QueryRowContext(ctx, qReports, reportArgs...).Scan(&draft, &submitted, &total); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	var upcoming int
	if scope.ViewOrg && scope.All {
		_ = s.DB.QueryRowContext(ctx,
			`SELECT COUNT(*) FROM calendar_events WHERE starts_at >= NOW() AND starts_at < NOW() + INTERVAL '30 days'`,
		).Scan(&upcoming)
	} else if scope.ViewOrg {
		divClause, divArgs, _ := reportsDivisionSQL("r", scope, 2)
		qCal := `SELECT COUNT(*) FROM calendar_events e
			LEFT JOIN reports r ON r.id = e.report_id
			WHERE e.starts_at >= NOW() AND e.starts_at < NOW() + INTERVAL '30 days'
			AND (e.user_id = $1 OR (e.report_id IS NOT NULL` + divClause + `))`
		args := append([]interface{}{uid}, divArgs...)
		_ = s.DB.QueryRowContext(ctx, qCal, args...).Scan(&upcoming)
	} else {
		_ = s.DB.QueryRowContext(ctx,
			`SELECT COUNT(*) FROM calendar_events WHERE user_id = $1 AND starts_at >= NOW() AND starts_at < NOW() + INTERVAL '30 days'`,
			uid,
		).Scan(&upcoming)
	}

	var notifUnread int
	_ = s.DB.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND read_at IS NULL`,
		uid,
	).Scan(&notifUnread)

	goalProgressPercent := 0
	if total > 0 {
		goalProgressPercent = (submitted * 100) / total
		if goalProgressPercent > 100 {
			goalProgressPercent = 100
		}
	}

	out := gin.H{
		"period":                 period,
		"reportsDraft":           draft,
		"reportsSubmitted":       submitted,
		"reportsTotal":           total,
		"reportsExpected":        total,
		"productivityScore":      goalProgressPercent,
		"goalProgressPercent":    goalProgressPercent,
		"calendarEventsUpcoming": upcoming,
		"notificationsUnread":    notifUnread,
	}

	incToday := strings.ToLower(strings.TrimSpace(c.Query("includeToday"))) == "1" || strings.ToLower(strings.TrimSpace(c.Query("includeToday"))) == "true"
	if incToday {
		var rid uint64
		var status string
		var reportDate sql.NullString
		var updatedAt time.Time
		err := s.DB.QueryRowContext(ctx,
			`SELECT r.id, r.status, r.report_date, r.updated_at FROM reports r
			 WHERE r.user_id = $1 AND (
			   r.report_date = CURRENT_DATE
			   OR (r.report_date IS NULL AND r.created_at::date = CURRENT_DATE)
			 )
			 ORDER BY r.updated_at DESC LIMIT 1`,
			uid,
		).Scan(&rid, &status, &reportDate, &updatedAt)
		if err == nil {
			tr := gin.H{"id": rid, "status": status, "updatedAt": updatedAt}
			if reportDate.Valid {
				tr["reportDate"] = reportDate.String
			}
			out["todayReport"] = tr
		} else if err != sql.ErrNoRows {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		} else {
			out["todayReport"] = nil
		}
	}

	c.JSON(http.StatusOK, httpapi.OK(out))
}

// getMeActivity returns a merged timeline of reports and notifications for the current user.
func (s *Server) getMeActivity(c *gin.Context) {
	uid := userID(c)
	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
	if limit < 1 {
		limit = 1
	}
	if limit > 50 {
		limit = 50
	}
	offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
	if offset < 0 {
		offset = 0
	}

	ctx, cancel := s.ctx(c)
	defer cancel()

	var isActive bool
	if err := s.DB.QueryRowContext(ctx, `SELECT is_active FROM users WHERE id = $1`, uid).Scan(&isActive); err != nil || !isActive {
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "account is disabled"))
		return
	}

	q := `
SELECT kind, id, title, description, at FROM (
  SELECT 'report'::text AS kind, id, title, status AS description, updated_at AS at
  FROM reports WHERE user_id = $1
  UNION ALL
  SELECT 'notification'::text, id, title, COALESCE(body, ''), created_at
  FROM notifications WHERE user_id = $1
) u
ORDER BY at DESC
LIMIT $2 OFFSET $3`

	rows, err := s.DB.QueryContext(ctx, q, uid, limit, offset)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()

	items := []gin.H{}
	for rows.Next() {
		var kind, title, desc string
		var id uint64
		var at time.Time
		if err := rows.Scan(&kind, &id, &title, &desc, &at); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		items = append(items, gin.H{
			"type":        kind,
			"id":          id,
			"title":       title,
			"description": strings.TrimSpace(desc),
			"at":          at,
		})
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	var total int
	_ = s.DB.QueryRowContext(ctx,
		`SELECT (SELECT COUNT(*) FROM reports WHERE user_id = $1) + (SELECT COUNT(*) FROM notifications WHERE user_id = $1)`,
		uid,
	).Scan(&total)

	c.JSON(http.StatusOK, httpapi.OK(gin.H{
		"items":   items,
		"total":   total,
		"limit":   limit,
		"offset":  offset,
		"hasMore": offset+len(items) < total,
	}))
}

// getMeFocusPreview returns a short mixed list of unread notifications and draft reports for dashboard "Focus".
func (s *Server) getMeFocusPreview(c *gin.Context) {
	uid := userID(c)
	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
	if limit < 1 {
		limit = 1
	}
	if limit > 20 {
		limit = 20
	}

	ctx, cancel := s.ctx(c)
	defer cancel()

	var isActive bool
	if err := s.DB.QueryRowContext(ctx, `SELECT is_active FROM users WHERE id = $1`, uid).Scan(&isActive); err != nil || !isActive {
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "account is disabled"))
		return
	}

	q := `
SELECT kind, id, title, subtitle, at FROM (
  SELECT 'notification'::text AS kind, id, title, COALESCE(body, '') AS subtitle, created_at AS at
  FROM notifications WHERE user_id = $1 AND read_at IS NULL
  UNION ALL
  SELECT 'report'::text, id, title, status, updated_at FROM reports WHERE user_id = $1 AND status = 'draft'
) u
ORDER BY at DESC
LIMIT $2`

	rows, err := s.DB.QueryContext(ctx, q, uid, limit)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()

	items := []gin.H{}
	for rows.Next() {
		var kind, title, subtitle string
		var id uint64
		var at time.Time
		if err := rows.Scan(&kind, &id, &title, &subtitle, &at); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		items = append(items, gin.H{
			"type":     kind,
			"id":       id,
			"title":    title,
			"subtitle": strings.TrimSpace(subtitle),
			"at":       at,
		})
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	c.JSON(http.StatusOK, httpapi.OK(gin.H{"items": items}))
}
