package api

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"

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

func respondAuthDBErr(c *gin.Context, err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, context.DeadlineExceeded) {
		c.JSON(http.StatusGatewayTimeout, httpapi.Fail("timeout", "request timed out"))
		return true
	}
	if errors.Is(err, context.Canceled) {
		c.JSON(http.StatusServiceUnavailable, httpapi.Fail("canceled", "request canceled"))
		return true
	}
	c.JSON(http.StatusInternalServerError, httpapi.Fail("db", "terjadi gangguan pada server"))
	return true
}

type loginBody struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required"`
}

type registerBody struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required,min=8"`
	Name     string `json:"name" binding:"required"`
}

type refreshBody struct {
	RefreshToken string `json:"refreshToken"`
}

func (s *Server) postLogin(c *gin.Context) {
	var body loginBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", httpapi.BindErrorMessage(err)))
		return
	}
	ctx, cancel := s.authCtx(c)
	defer cancel()
	var id uint64
	var hash sql.NullString
	var name, email, role string
	var isActive bool
	err := s.DB.QueryRowContext(ctx, `SELECT id, email, name, password_hash, role, is_active FROM users WHERE email = $1`, strings.ToLower(body.Email)).Scan(&id, &email, &name, &hash, &role, &isActive)
	if err == sql.ErrNoRows || !hash.Valid {
		c.JSON(http.StatusUnauthorized, httpapi.Fail("invalid_credentials", "invalid email or password"))
		return
	}
	if respondAuthDBErr(c, err) {
		return
	}
	if !isActive {
		c.JSON(http.StatusForbidden, httpapi.Fail("account_disabled", "account is disabled"))
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(hash.String), []byte(body.Password)) != nil {
		c.JSON(http.StatusUnauthorized, httpapi.Fail("invalid_credentials", "invalid email or password"))
		return
	}
	s.issueSession(c, ctx, id, email, name, role)
	s.recordAuthActivity(c, ctx, id, "auth.login", "Signed in", "Email & password", email, name)
}

func (s *Server) postRegister(c *gin.Context) {
	var body registerBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", httpapi.BindErrorMessage(err)))
		return
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("crypto", err.Error()))
		return
	}
	ctx, cancel := s.authCtx(c)
	defer cancel()
	var id uint64
	err = s.DB.QueryRowContext(ctx, `INSERT INTO users (email, password_hash, name) VALUES ($1,$2,$3) RETURNING id`,
		strings.ToLower(body.Email), string(hash), body.Name).Scan(&id)
	if err != nil {
		if strings.Contains(err.Error(), "23505") || strings.Contains(strings.ToLower(err.Error()), "duplicate") {
			c.JSON(http.StatusConflict, httpapi.Fail("duplicate", "email already registered"))
			return
		}
		if respondAuthDBErr(c, err) {
			return
		}
	}
	email := strings.ToLower(body.Email)
	s.issueSession(c, ctx, id, email, body.Name, "user")
	s.recordAuthActivity(c, ctx, id, "auth.register", "Account created", "Registration", email, body.Name)
}

func (s *Server) issueSession(c *gin.Context, ctx context.Context, id uint64, email, name, role string) {
	access, err := authjwt.SignAccess(s.Cfg.JWTSecret, id, email, role, s.Cfg.JWTAccessTTL)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("token", err.Error()))
		return
	}
	raw, hash, err := authjwt.RandomToken()
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("token", err.Error()))
		return
	}
	_, err = s.DB.ExecContext(ctx, `INSERT INTO refresh_sessions (user_id, token_hash, expires_at) VALUES ($1,$2,$3)`,
		id, hash, time.Now().Add(s.Cfg.JWTRefreshTTL))
	if respondAuthDBErr(c, err) {
		return
	}
	s.setAuthCookies(c, access, raw)
	perms, _ := s.loadUserPermissions(ctx, id, role)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{
		"accessToken":  access,
		"refreshToken": raw,
		"user": gin.H{
			"id":          id,
			"email":       email,
			"name":        name,
			"role":        role,
			"permissions": permissionsForResponse(perms),
		},
	}))
}

func (s *Server) postRefresh(c *gin.Context) {
	bodyBytes, _ := io.ReadAll(c.Request.Body)
	c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
	var body refreshBody
	if len(bytes.TrimSpace(bodyBytes)) > 0 {
		if err := json.Unmarshal(bodyBytes, &body); err != nil {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
			return
		}
	}
	if strings.TrimSpace(body.RefreshToken) == "" {
		body.RefreshToken = refreshTokenFromRequest(c)
	}
	if strings.TrimSpace(body.RefreshToken) == "" {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "refreshToken required in body or refresh_token cookie"))
		return
	}
	h := authjwt.HashToken(body.RefreshToken)
	ctx, cancel := s.authCtx(c)
	defer cancel()
	var id uint64
	var email, name, role string
	err := s.DB.QueryRowContext(ctx, `SELECT u.id, u.email, u.name, u.role FROM refresh_sessions r JOIN users u ON u.id = r.user_id WHERE r.token_hash = $1 AND r.revoked = false AND r.expires_at > NOW() AND u.is_active = true`, h).Scan(&id, &email, &name, &role)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusUnauthorized, httpapi.Fail("invalid_refresh", "invalid refresh token"))
		return
	}
	if respondAuthDBErr(c, err) {
		return
	}
	if _, err := s.DB.ExecContext(ctx, `UPDATE refresh_sessions SET revoked = true WHERE token_hash = $1`, h); respondAuthDBErr(c, err) {
		return
	}
	newRT, nhash, err := authjwt.RandomToken()
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("token", err.Error()))
		return
	}
	_, err = s.DB.ExecContext(ctx, `INSERT INTO refresh_sessions (user_id, token_hash, expires_at) VALUES ($1,$2,$3)`,
		id, nhash, time.Now().Add(s.Cfg.JWTRefreshTTL))
	if respondAuthDBErr(c, err) {
		return
	}
	access, err := authjwt.SignAccess(s.Cfg.JWTSecret, id, email, role, s.Cfg.JWTAccessTTL)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("token", err.Error()))
		return
	}
	s.setAuthCookies(c, access, newRT)
	perms, _ := s.loadUserPermissions(ctx, id, role)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{
		"accessToken":  access,
		"refreshToken": newRT,
		"user": gin.H{
			"id":          id,
			"email":       email,
			"name":        name,
			"role":        role,
			"permissions": permissionsForResponse(perms),
		},
	}))
}

func (s *Server) revokeAllRefreshSessions(ctx context.Context, userID uint64) error {
	_, err := s.DB.ExecContext(ctx, `UPDATE refresh_sessions SET revoked = true WHERE user_id = $1 AND revoked = false`, userID)
	return err
}

func (s *Server) postLogout(c *gin.Context) {
	rawBody, _ := io.ReadAll(c.Request.Body)
	c.Request.Body = io.NopCloser(bytes.NewReader(rawBody))

	var req struct {
		Everywhere   bool   `json:"everywhere"`
		RefreshToken string `json:"refreshToken"`
	}
	if len(bytes.TrimSpace(rawBody)) > 0 {
		if err := json.Unmarshal(rawBody, &req); err != nil {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
			return
		}
	}

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

	uidAccess, hasAccess := s.accessUserID(c)
	var disconnectUID uint64

	if req.Everywhere {
		if !hasAccess {
			s.clearAuthCookies(c)
			c.JSON(http.StatusUnauthorized, httpapi.Fail("unauthorized", "everywhere logout requires a valid access token (Bearer or access_token cookie)"))
			return
		}
		if err := s.revokeAllRefreshSessions(ctx, uidAccess); err != nil {
			s.clearAuthCookies(c)
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		disconnectUID = uidAccess
	} else {
		rt := strings.TrimSpace(req.RefreshToken)
		if rt == "" {
			rt = refreshTokenFromRequest(c)
		}
		if rt != "" {
			h := authjwt.HashToken(rt)
			var uid uint64
			err := s.DB.QueryRowContext(ctx, `SELECT user_id FROM refresh_sessions WHERE token_hash = $1 AND revoked = false LIMIT 1`, h).Scan(&uid)
			if err != nil && err != sql.ErrNoRows {
				s.clearAuthCookies(c)
				c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
				return
			}
			if err == nil {
				if _, err := s.DB.ExecContext(ctx, `UPDATE refresh_sessions SET revoked = true WHERE token_hash = $1`, h); err != nil {
					s.clearAuthCookies(c)
					c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
					return
				}
				disconnectUID = uid
			}
		} else if hasAccess {
			if err := s.revokeAllRefreshSessions(ctx, uidAccess); err != nil {
				s.clearAuthCookies(c)
				c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
				return
			}
			disconnectUID = uidAccess
		}
	}

	if disconnectUID != 0 {
		s.Hub.DisconnectUser(disconnectUID)
		realtime.PublishRedis(ctx, s.Cfg, fmt.Sprintf("workpulse:user:%d", disconnectUID), "session.logout")
		var email, name string
		_ = s.DB.QueryRowContext(ctx, `SELECT email, COALESCE(NULLIF(TRIM(name), ''), email) FROM users WHERE id = $1`, disconnectUID).Scan(&email, &name)
		if req.Everywhere {
			s.recordAuthActivity(c, ctx, disconnectUID, "auth.logout_all", "Signed out everywhere", "All devices", email, name)
		} else {
			s.recordAuthActivity(c, ctx, disconnectUID, "auth.logout", "Signed out", "This device", email, name)
		}
	}

	s.clearAuthCookies(c)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"loggedOut": true}))
}

func (s *Server) googleOAuth() *oauth2.Config {
	return &oauth2.Config{
		ClientID:     s.Cfg.GoogleClientID,
		ClientSecret: s.Cfg.GoogleClientSecret,
		RedirectURL:  s.Cfg.GoogleRedirectURL,
		Scopes:       []string{"openid", "email", "profile"},
		Endpoint:     google.Endpoint,
	}
}

func (s *Server) getGoogleURL(c *gin.Context) {
	if strings.TrimSpace(s.Cfg.GoogleClientID) == "" {
		c.JSON(http.StatusServiceUnavailable, httpapi.Fail("oauth_unconfigured", "Google OAuth is not configured"))
		return
	}
	state, err := authjwt.SignOAuthState(s.Cfg.JWTSecret, 10*time.Minute)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("state", err.Error()))
		return
	}
	url := s.googleOAuth().AuthCodeURL(state, oauth2.AccessTypeOffline)
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"url": url}))
}

func (s *Server) getGoogleCallback(c *gin.Context) {
	code := c.Query("code")
	state := c.Query("state")
	if code == "" || state == "" {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "missing code or state"))
		return
	}
	if err := authjwt.ParseOAuthState(s.Cfg.JWTSecret, state); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("state", "invalid state"))
		return
	}
	if strings.TrimSpace(s.Cfg.GoogleClientID) == "" {
		c.JSON(http.StatusServiceUnavailable, httpapi.Fail("oauth_unconfigured", "Google OAuth is not configured"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	tok, err := s.googleOAuth().Exchange(ctx, code)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("oauth", err.Error()))
		return
	}
	cli := s.googleOAuth().Client(ctx, tok)
	resp, err := cli.Get("https://www.googleapis.com/oauth2/v3/userinfo")
	if err != nil {
		c.JSON(http.StatusBadGateway, httpapi.Fail("google", err.Error()))
		return
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != 200 {
		c.JSON(http.StatusBadGateway, httpapi.Fail("google", fmt.Sprintf("userinfo: %s", string(b))))
		return
	}
	var ui struct {
		Sub     string `json:"sub"`
		Email   string `json:"email"`
		Name    string `json:"name"`
		Picture string `json:"picture"`
	}
	if err := json.Unmarshal(b, &ui); err != nil {
		c.JSON(http.StatusBadGateway, httpapi.Fail("google", err.Error()))
		return
	}
	email := strings.ToLower(strings.TrimSpace(ui.Email))
	if email == "" {
		c.JSON(http.StatusBadGateway, httpapi.Fail("google", "email not returned by Google"))
		return
	}
	var id uint64
	err = s.DB.QueryRowContext(ctx, `SELECT id FROM users WHERE google_sub = $1`, ui.Sub).Scan(&id)
	if err != nil && err != sql.ErrNoRows {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if err == sql.ErrNoRows {
		err = s.DB.QueryRowContext(ctx, `SELECT id FROM users WHERE email = $1`, email).Scan(&id)
		if err != nil && err != sql.ErrNoRows {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	}
	if err == sql.ErrNoRows {
		err = s.DB.QueryRowContext(ctx,
			`INSERT INTO users (email, name, avatar_url, google_sub) VALUES ($1,$2,$3,$4) RETURNING id`,
			email, ui.Name, nullStr(ui.Picture), ui.Sub).Scan(&id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	} else {
		_, _ = s.DB.ExecContext(ctx, `UPDATE users SET google_sub = COALESCE(google_sub, $1), name = COALESCE(NULLIF($2, ''), name), avatar_url = COALESCE($3, avatar_url) WHERE id = $4`,
			ui.Sub, ui.Name, nullStr(ui.Picture), id)
	}
	var name, role string
	_ = s.DB.QueryRowContext(ctx, `SELECT name, role FROM users WHERE id = $1`, id).Scan(&name, &role)
	s.issueSession(c, ctx, id, email, name, role)
	s.recordAuthActivity(c, ctx, id, "auth.oauth_google", "Signed in with Google", "Google OAuth", email, name)
}

func nullStr(s string) interface{} {
	if strings.TrimSpace(s) == "" {
		return nil
	}
	return s
}
