package api

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

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

func userID(c *gin.Context) uint64 {
	v, _ := c.Get("userID")
	return v.(uint64)
}

// --- Reports ---

func (s *Server) listReports(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	var b strings.Builder
	var args []interface{}
	n := 1
	b.WriteString(`SELECT id, user_id, team_id, title, body, status, division, report_date, created_at, updated_at FROM reports WHERE user_id = $`)
	b.WriteString(strconv.Itoa(n))
	args = append(args, uid)
	n++
	if d := c.Query("division"); d != "" {
		b.WriteString(` AND division = $`)
		b.WriteString(strconv.Itoa(n))
		args = append(args, d)
		n++
	}
	if fd := c.Query("from"); fd != "" {
		b.WriteString(` AND (report_date IS NULL OR report_date >= $`)
		b.WriteString(strconv.Itoa(n))
		b.WriteString(`)`)
		args = append(args, fd)
		n++
	}
	if td := c.Query("to"); td != "" {
		b.WriteString(` AND (report_date IS NULL OR report_date <= $`)
		b.WriteString(strconv.Itoa(n))
		b.WriteString(`)`)
		args = append(args, td)
		n++
	}
	b.WriteString(` ORDER BY updated_at DESC LIMIT 200`)
	rows, err := s.DB.QueryContext(ctx, b.String(), args...)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	var out []gin.H
	for rows.Next() {
		var id, userID uint64
		var teamID sql.NullInt64
		var title, body, status string
		var division sql.NullString
		var reportDate sql.NullString
		var created, updated time.Time
		if err := rows.Scan(&id, &userID, &teamID, &title, &body, &status, &division, &reportDate, &created, &updated); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		h := gin.H{"id": id, "userId": userID, "title": title, "body": body, "status": status, "createdAt": created, "updatedAt": updated}
		if teamID.Valid {
			h["teamId"] = uint64(teamID.Int64)
		}
		if division.Valid {
			h["division"] = division.String
		}
		if reportDate.Valid {
			h["reportDate"] = reportDate.String
		}
		out = append(out, h)
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

type reportCreate struct {
	Title      string  `json:"title" binding:"required"`
	Body       string  `json:"body"`
	Status     string  `json:"status"`
	Division   string  `json:"division"`
	ReportDate string  `json:"reportDate"`
	TeamID     *uint64 `json:"teamId"`
}

func (s *Server) createReport(c *gin.Context) {
	var body reportCreate
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	st := body.Status
	if st == "" {
		st = "draft"
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	var tid interface{}
	if body.TeamID != nil {
		ok, err := s.IsTeamMember(ctx, uid, *body.TeamID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		if !ok {
			c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "not a member of this team"))
			return
		}
		tid = *body.TeamID
	} else {
		tid = nil
	}
	var rd interface{}
	if strings.TrimSpace(body.ReportDate) != "" {
		rd = body.ReportDate
	} else {
		rd = nil
	}
	divCanon, err := s.normalizeReportDivision(ctx, body.Division)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	var lid int64
	err = s.DB.QueryRowContext(ctx,
		`INSERT INTO reports (user_id, team_id, title, body, status, division, report_date) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
		uid, tid, body.Title, body.Body, st, nullStr(divCanon), rd).Scan(&lid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if err := s.syncReportCalendarEvent(ctx, uint64(lid)); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if reportStatusSubmitted(st) {
		_ = s.notifyOnReportSubmitted(ctx, uint64(lid), uid)
	}
	payload := `{"type":"report.created","id":` + strconv.FormatInt(lid, 10) + `}`
	realtime.PublishRedis(ctx, s.Cfg, "workpulse:broadcast", payload)
	if body.TeamID != nil {
		realtime.PublishRedis(ctx, s.Cfg, fmt.Sprintf("workpulse:team:%d", *body.TeamID), payload)
	}
	s.Hub.BroadcastEvent("report.created", gin.H{"id": lid})
	s.recordUserActivity(c, ctx, uid, "report.created", "Daily report created", body.Title, gin.H{"reportId": lid, "status": st})
	c.JSON(http.StatusCreated, httpapi.OK(gin.H{"id": lid}))
}

func (s *Server) getReport(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid id"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	var r struct {
		ID, UserID          uint64
		TeamID              sql.NullInt64
		Title, Body, Status string
		Division            sql.NullString
		ReportDate          sql.NullString
		Created, Updated    time.Time
	}
	err = s.DB.QueryRowContext(ctx, `SELECT id, user_id, team_id, title, body, status, division, report_date, created_at, updated_at FROM reports WHERE id = $1`, id).
		Scan(&r.ID, &r.UserID, &r.TeamID, &r.Title, &r.Body, &r.Status, &r.Division, &r.ReportDate, &r.Created, &r.Updated)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "report not found"))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if r.UserID != uid && !roleIsSuperadmin(c) {
		if r.Division.Valid {
			ok, err := s.requestCanAccessDivision(ctx, c, r.Division.String)
			if err == nil && ok {
				h := gin.H{"id": r.ID, "userId": r.UserID, "title": r.Title, "body": r.Body, "status": r.Status, "createdAt": r.Created, "updatedAt": r.Updated}
				if r.TeamID.Valid {
					h["teamId"] = uint64(r.TeamID.Int64)
				}
				h["division"] = r.Division.String
				if r.ReportDate.Valid {
					h["reportDate"] = r.ReportDate.String
				}
				c.JSON(http.StatusOK, httpapi.OK(h))
				return
			}
			if err != nil {
				c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
				return
			}
		}
		if !r.TeamID.Valid {
			c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "cannot access this report"))
			return
		}
		ok, err := s.IsTeamMember(ctx, uid, uint64(r.TeamID.Int64))
		if err != nil || !ok {
			c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "cannot access this report"))
			return
		}
	}
	h := gin.H{"id": r.ID, "userId": r.UserID, "title": r.Title, "body": r.Body, "status": r.Status, "createdAt": r.Created, "updatedAt": r.Updated}
	if r.TeamID.Valid {
		h["teamId"] = uint64(r.TeamID.Int64)
	}
	if r.Division.Valid {
		h["division"] = r.Division.String
	}
	if r.ReportDate.Valid {
		h["reportDate"] = r.ReportDate.String
	}
	c.JSON(http.StatusOK, httpapi.OK(h))
}

type reportPatch struct {
	Title      *string `json:"title"`
	Body       *string `json:"body"`
	Status     *string `json:"status"`
	Division   *string `json:"division"`
	ReportDate *string `json:"reportDate"`
}

func (s *Server) patchReport(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid id"))
		return
	}
	var body reportPatch
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	owner, ok := s.reportOwnerOrSuperadmin(c, ctx, id)
	if !ok {
		return
	}
	var prevStatus string
	if err := s.DB.QueryRowContext(ctx, `SELECT status FROM reports WHERE id = $1`, id).Scan(&prevStatus); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	becameSubmitted := false
	if body.Title != nil {
		_, _ = s.DB.ExecContext(ctx, `UPDATE reports SET title = $1 WHERE id = $2`, *body.Title, id)
	}
	if body.Body != nil {
		bodyToStore := *body.Body
		var currentBody string
		if err := s.DB.QueryRowContext(ctx, `SELECT body FROM reports WHERE id = $1`, id).Scan(&currentBody); err == nil {
			if merged, merr := mergeReportBodyPreserveAttachments(currentBody, bodyToStore); merr == nil {
				bodyToStore = merged
			}
		}
		_, _ = s.DB.ExecContext(ctx, `UPDATE reports SET body = $1 WHERE id = $2`, bodyToStore, id)
	}
	if body.Status != nil {
		newSt := strings.ToLower(strings.TrimSpace(*body.Status))
		oldSt := strings.ToLower(strings.TrimSpace(prevStatus))
		_, _ = s.DB.ExecContext(ctx, `UPDATE reports SET status = $1 WHERE id = $2`, *body.Status, id)
		if newSt == "submitted" && oldSt != "submitted" {
			becameSubmitted = true
		}
	}
	if body.Division != nil {
		divCanon, err := s.normalizeReportDivision(ctx, *body.Division)
		if err != nil {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
			return
		}
		_, err = s.DB.ExecContext(ctx, `UPDATE reports SET division = $1 WHERE id = $2`, nullStr(divCanon), id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	}
	if body.ReportDate != nil {
		rd := strings.TrimSpace(*body.ReportDate)
		var execErr error
		if rd == "" {
			_, execErr = s.DB.ExecContext(ctx, `UPDATE reports SET report_date = NULL WHERE id = $1`, id)
		} else {
			_, execErr = s.DB.ExecContext(ctx, `UPDATE reports SET report_date = $1::date WHERE id = $2`, rd, id)
		}
		if execErr != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", execErr.Error()))
			return
		}
	}
	if err := s.syncReportCalendarEvent(ctx, id); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if becameSubmitted {
		_ = s.notifyOnReportSubmitted(ctx, id, owner)
		s.recordUserActivity(c, ctx, uid, "report.submitted", "Daily report submitted", "", gin.H{"reportId": id})
	} else if roleIsSuperadmin(c) && owner != uid {
		s.recordUserActivity(c, ctx, uid, "report.admin_updated", "Daily report corrected (admin)", "", gin.H{"reportId": id, "ownerUserId": owner})
	} else {
		s.recordUserActivity(c, ctx, uid, "report.updated", "Daily report updated", "", gin.H{"reportId": id})
	}
	s.Hub.BroadcastEvent("report.updated", gin.H{"id": id})
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"id": id}))
}

// --- Teams ---

func (s *Server) listTeams(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	rows, err := s.DB.QueryContext(ctx, `SELECT t.id, t.name, t.slug, t.created_at FROM teams t INNER JOIN team_members m ON m.team_id = t.id WHERE m.user_id = $1 ORDER BY t.name`, uid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	out := []gin.H{}
	for rows.Next() {
		var id uint64
		var name, slug string
		var created time.Time
		if err := rows.Scan(&id, &name, &slug, &created); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		out = append(out, gin.H{"id": id, "name": name, "slug": slug, "createdAt": created})
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

func (s *Server) teamMembers(c *gin.Context) {
	tid, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid team id"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	ok, err := s.IsTeamMember(ctx, uid, tid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if !ok {
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "not a team member"))
		return
	}
	rows, err := s.DB.QueryContext(ctx, `SELECT u.id, u.email, u.name, m.role, u.is_active FROM team_members m JOIN users u ON u.id = m.user_id WHERE m.team_id = $1`, tid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	out := []gin.H{}
	for rows.Next() {
		var id uint64
		var email, name, role string
		var isActive bool
		if err := rows.Scan(&id, &email, &name, &role, &isActive); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		out = append(out, gin.H{"id": id, "email": email, "name": name, "role": role, "isActive": isActive})
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

// teamSummary aggregates report counts for all members of a team in the same period semantics as GET /me/summary.
func (s *Server) teamSummary(c *gin.Context) {
	tid, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid team id"))
		return
	}
	period := c.DefaultQuery("period", "month")
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	ok, err := s.IsTeamMember(ctx, uid, tid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if !ok {
		c.JSON(http.StatusForbidden, httpapi.Fail("forbidden", "not a team member"))
		return
	}

	where := meReportsPeriodWhere("r", period)
	q := `SELECT 
		COUNT(*) FILTER (WHERE r.status = 'draft') AS draft,
		COUNT(*) FILTER (WHERE r.status <> 'draft') AS submitted,
		COUNT(*) AS total
		FROM reports r
		INNER JOIN team_members m ON m.user_id = r.user_id AND m.team_id = $1
		WHERE (` + where + `)`

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

	c.JSON(http.StatusOK, httpapi.OK(gin.H{
		"teamId":           tid,
		"period":           period,
		"reportsDraft":     draft,
		"reportsSubmitted": submitted,
		"reportsTotal":     total,
	}))
}

// --- Calendar ---

func (s *Server) listCalendar(c *gin.Context) {
	from := c.Query("from")
	to := c.Query("to")
	if from == "" || to == "" {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "from and to query params required (RFC3339 or YYYY-MM-DD)"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)

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

	q := `SELECT e.id, e.user_id, e.team_id, e.title, e.starts_at, e.ends_at, e.all_day, e.created_at, e.report_id, e.kind, t.name AS team_name,
COALESCE(NULLIF(TRIM(u.name), ''), NULLIF(TRIM(u.email), ''), 'User ' || e.user_id::text) AS organizer_name,
u.email AS organizer_email, u.avatar_url AS organizer_avatar_url, r.report_date, r.status, r.division, r.body
FROM calendar_events e
LEFT JOIN teams t ON t.id = e.team_id
JOIN users u ON u.id = e.user_id
LEFT JOIN reports r ON r.id = e.report_id
WHERE e.starts_at < $1 AND e.ends_at > $2`
	args := []interface{}{to, from}
	n := 3

	if scope.ViewOrg && scope.All {
		// superadmin / semua divisi: seluruh acara organisasi
	} else if scope.ViewOrg {
		q += ` AND (e.user_id = $` + strconv.Itoa(n)
		args = append(args, uid)
		n++
		divClause, divArgs, nextN := reportsDivisionSQL("r", scope, n)
		n = nextN
		q += ` OR (e.report_id IS NOT NULL` + divClause + `))`
		args = append(args, divArgs...)
	} else {
		q += ` AND e.user_id = $` + strconv.Itoa(n)
		args = append(args, uid)
		n++
	}

	if teamStr := strings.TrimSpace(c.Query("teamId")); teamStr != "" {
		tid, err := strconv.ParseUint(teamStr, 10, 64)
		if err != nil || tid == 0 {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid teamId"))
			return
		}
		q += ` AND e.team_id = $` + strconv.Itoa(n)
		args = append(args, tid)
	}
	q += ` ORDER BY e.starts_at`

	rows, err := s.DB.QueryContext(ctx, q, args...)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	out := []gin.H{}
	for rows.Next() {
		var id, rowUserID uint64
		var teamID sql.NullInt64
		var title string
		var teamName, organizerName, organizerEmail, organizerAvatarURL sql.NullString
		var reportID sql.NullInt64
		var reportDate, reportStatus, reportDivision, reportBody sql.NullString
		var kind string
		var start, end time.Time
		var allDay bool
		var created time.Time
		if err := rows.Scan(&id, &rowUserID, &teamID, &title, &start, &end, &allDay, &created, &reportID, &kind, &teamName, &organizerName, &organizerEmail, &organizerAvatarURL, &reportDate, &reportStatus, &reportDivision, &reportBody); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		h := gin.H{"id": id, "userId": rowUserID, "title": title, "startsAt": start, "endsAt": end, "allDay": allDay, "createdAt": created, "kind": kind}
		h["organizerName"] = strings.TrimSpace(organizerName.String)
		if organizerEmail.Valid && strings.TrimSpace(organizerEmail.String) != "" {
			h["organizerEmail"] = strings.TrimSpace(organizerEmail.String)
		}
		if organizerAvatarURL.Valid && strings.TrimSpace(organizerAvatarURL.String) != "" {
			h["organizerAvatarUrl"] = strings.TrimSpace(organizerAvatarURL.String)
		}
		if teamID.Valid {
			h["teamId"] = uint64(teamID.Int64)
		}
		if reportID.Valid {
			h["reportId"] = uint64(reportID.Int64)
		}
		if reportDate.Valid && strings.TrimSpace(reportDate.String) != "" {
			rd := strings.TrimSpace(reportDate.String)
			if len(rd) > 10 {
				rd = rd[:10]
			}
			h["reportDate"] = rd
		}
		if reportID.Valid {
			if reportStatus.Valid && strings.TrimSpace(reportStatus.String) != "" {
				h["reportStatus"] = strings.TrimSpace(reportStatus.String)
			}
			if reportDivision.Valid && strings.TrimSpace(reportDivision.String) != "" {
				h["reportDivision"] = strings.TrimSpace(reportDivision.String)
			}
			if reportBody.Valid {
				h["summary"] = orgReportBodyListSummary(reportBody.String)
			}
		}
		if teamName.Valid && strings.TrimSpace(teamName.String) != "" {
			h["teamName"] = teamName.String
		}
		out = append(out, h)
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

// --- Notifications ---

func (s *Server) listNotifications(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	rows, err := s.DB.QueryContext(ctx, `SELECT id, title, body, read_at, created_at FROM notifications WHERE user_id = $1 ORDER BY created_at DESC LIMIT 100`, uid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	out := []gin.H{}
	for rows.Next() {
		var id uint64
		var title string
		var body sql.NullString
		var readAt sql.NullTime
		var created time.Time
		if err := rows.Scan(&id, &title, &body, &readAt, &created); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		bodyStr := ""
		if body.Valid {
			bodyStr = body.String
		}
		h := gin.H{"id": id, "title": title, "body": bodyStr, "createdAt": created, "read": readAt.Valid}
		if readAt.Valid {
			h["readAt"] = readAt.Time
		}
		out = append(out, h)
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

func (s *Server) patchNotificationsRead(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	_, err := s.DB.ExecContext(ctx, `UPDATE notifications SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL`, uid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"ok": true}))
}

// patchNotificationReadOne marks a single notification as read for the current user.
func (s *Server) patchNotificationReadOne(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	idStr := c.Param("id")
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil || id == 0 {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid notification id"))
		return
	}
	res, err := s.DB.ExecContext(ctx,
		`UPDATE notifications SET read_at = NOW() WHERE id = $1 AND user_id = $2 AND read_at IS NULL`,
		id, uid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	n, _ := res.RowsAffected()
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"updated": n > 0}))
}

// --- Analytics ---

func (s *Server) analyticsSummary(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	scope, err := s.divisionScopeFromContext(ctx, c)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	var reports, notifUnread int
	if scope.ViewOrg {
		divClause, divArgs, _ := reportsDivisionSQL("r", scope, 1)
		q := `SELECT COUNT(*) FROM reports r WHERE TRUE` + divClause
		_ = s.DB.QueryRowContext(ctx, q, divArgs...).Scan(&reports)
	} else {
		_ = s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM reports WHERE user_id = $1`, uid).Scan(&reports)
	}
	_ = s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND read_at IS NULL`, uid).Scan(&notifUnread)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{
		"reportsTotal":        reports,
		"notificationsUnread": notifUnread,
	}))
}

func (s *Server) analyticsTrends(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)

	days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))
	if days < 1 {
		days = 1
	}
	if days > 90 {
		days = 90
	}
	align := strings.ToLower(strings.TrimSpace(c.DefaultQuery("align", "")))

	var timePred string
	if align == "week" {
		timePred = `r.created_at >= date_trunc('week', CURRENT_TIMESTAMP) AND r.created_at < date_trunc('week', CURRENT_TIMESTAMP) + INTERVAL '7 days'`
	} else {
		timePred = fmt.Sprintf(`r.created_at > NOW() - INTERVAL '%d days'`, days)
	}

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

	var q string
	var queryArgs []interface{}
	if scope.ViewOrg {
		divClause, divArgs, _ := reportsDivisionSQL("r", scope, 1)
		q = `SELECT (r.created_at::date)::text AS d, COUNT(*) AS c
		FROM reports r
		WHERE (` + timePred + `)` + divClause + `
		GROUP BY r.created_at::date ORDER BY d`
		queryArgs = divArgs
	} else {
		q = `SELECT (r.created_at::date)::text AS d, COUNT(*) AS c
		FROM reports r
		WHERE r.user_id = $1 AND (` + timePred + `)
		GROUP BY r.created_at::date ORDER BY d`
		queryArgs = []interface{}{uid}
	}

	rows, err := s.DB.QueryContext(ctx, q, queryArgs...)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()
	var series []gin.H
	for rows.Next() {
		var d string
		var cnt int
		if err := rows.Scan(&d, &cnt); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		series = append(series, gin.H{"date": d, "count": cnt})
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	out := gin.H{"reportsByDay": series, "days": days}
	if align == "week" {
		out["align"] = "week"
	}
	c.JSON(http.StatusOK, httpapi.OK(out))
}

// --- Settings ---

func (s *Server) getSettings(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	var raw []byte
	err := s.DB.QueryRowContext(ctx, `SELECT preferences_json FROM user_settings WHERE user_id = $1`, uid).Scan(&raw)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusOK, httpapi.OK(gin.H{"preferences": map[string]interface{}{}}))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	var m map[string]interface{}
	_ = json.Unmarshal(raw, &m)
	if m == nil {
		m = map[string]interface{}{}
	}
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"preferences": m}))
}

func (s *Server) patchSettings(c *gin.Context) {
	var body struct {
		Preferences map[string]interface{} `json:"preferences" binding:"required"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	b, err := json.Marshal(body.Preferences)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	_, err = s.DB.ExecContext(ctx,
		`INSERT INTO user_settings (user_id, preferences_json) VALUES ($1, $2::jsonb)
		 ON CONFLICT (user_id) DO UPDATE SET preferences_json = EXCLUDED.preferences_json, updated_at = NOW()`,
		uid, b)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	s.recordUserActivity(c, ctx, uid, "preferences.save", "Preferences saved", "Theme or language settings updated", nil)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"preferences": body.Preferences}))
}

// --- Profile ---

func (s *Server) getProfile(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	var email, name, role string
	var avatar sql.NullString
	err := s.DB.QueryRowContext(ctx, `SELECT email, name, avatar_url, role FROM users WHERE id = $1`, uid).Scan(&email, &name, &avatar, &role)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	h := gin.H{"id": uid, "email": email, "name": name, "role": role}
	if avatar.Valid {
		h["avatarUrl"] = avatar.String
	}
	c.JSON(http.StatusOK, httpapi.OK(h))
}

func (s *Server) patchProfile(c *gin.Context) {
	var body struct {
		Name      *string `json:"name"`
		AvatarURL *string `json:"avatarUrl"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	uid := userID(c)
	if body.Name != nil {
		_, _ = s.DB.ExecContext(ctx, `UPDATE users SET name = $1 WHERE id = $2`, *body.Name, uid)
	}
	if body.AvatarURL != nil {
		_, _ = s.DB.ExecContext(ctx, `UPDATE users SET avatar_url = $1 WHERE id = $2`, *body.AvatarURL, uid)
	}
	s.getProfile(c)
}
