package api

import (
	"context"
	"encoding/json"
	"net/http"
	"strings"
	"time"

	"github.com/ponzischeme89/memby/server/internal/cache"
	"github.com/ponzischeme89/memby/server/internal/emby"
	"github.com/ponzischeme89/memby/server/internal/store"
)

type loginRequest struct {
	Username   string `json:"username"`
	Password   string `json:"password"`
	DeviceID   string `json:"deviceId"`
	DeviceName string `json:"deviceName"`
}

type loginResponse struct {
	Token    string `json:"token"`
	UserID   string `json:"userId"`
	Username string `json:"username"`
	ServerID string `json:"serverId"`
}

type deviceSessionResponse struct {
	DeviceID      string    `json:"deviceId"`
	DeviceName    string    `json:"deviceName"`
	ClientVersion string    `json:"clientVersion,omitempty"`
	LastSeenAt    time.Time `json:"lastSeenAt"`
	Current       bool      `json:"current"`
}

type renameDeviceRequest struct {
	DeviceName string `json:"deviceName"`
}

// handleLogin exchanges Emby credentials for a gateway token.
//
// The Emby access token stays here: the TV only ever holds the gateway token, so
// revoking a device is a DELETE in Postgres rather than an Emby-side cleanup.
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
	var req loginRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}
	req.Username = strings.TrimSpace(req.Username)
	if req.Username == "" {
		writeError(w, http.StatusBadRequest, "username is required")
		return
	}
	if req.DeviceID == "" {
		req.DeviceID = "memby-tv"
	}
	req.DeviceName = strings.TrimSpace(req.DeviceName)
	if req.DeviceName == "" {
		// Compatibility for APKs released before device naming. New clients require an
		// editable name in their UI, but an older TV must still be able to sign in while
		// the household rollout is in progress.
		req.DeviceName = store.DefaultDeviceName
	}
	if len([]rune(req.DeviceName)) > 80 {
		writeError(w, http.StatusBadRequest, "device name is too long")
		return
	}

	auth, err := s.emby.Authenticate(
		r.Context(), req.Username, req.Password, req.DeviceID, req.DeviceName, clientVersion(r),
	)
	if err != nil {
		// Never echo Emby's body here: a failed sign-in is the one place a wrong
		// password could be reflected back.
		s.loggerFor(r.Context()).Warn("sign-in rejected",
			"username", req.Username, "device", req.DeviceName, "device_id", req.DeviceID,
			"reason", "emby refused the credentials",
		)
		writeError(w, http.StatusUnauthorized, "sign-in failed")
		return
	}

	token, err := newToken()
	if err != nil {
		s.log.Error("token generation failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not issue a token")
		return
	}

	sess := store.Session{
		TokenHash:          hashToken(token),
		EmbyUserID:         auth.User.ID,
		EmbyToken:          auth.AccessToken,
		Username:           auth.User.Name,
		ServerID:           auth.ServerID,
		DeviceID:           req.DeviceID,
		DeviceName:         req.DeviceName,
		ClientVersion:      clientVersion(r),
		ClientProtocol:     clientProtocol(r),
		ClientCapabilities: clientCapabilities(r),
	}
	if sess.Username == "" {
		sess.Username = req.Username
	}
	created, err := s.store.CreateSession(r.Context(), sess)
	if err != nil {
		_ = s.emby.Logout(r.Context(), emby.Credentials{
			UserID: auth.User.ID, Token: auth.AccessToken,
			DeviceID: req.DeviceID, DeviceName: req.DeviceName,
			ClientVersion: sess.ClientVersion,
		})
		s.log.Error("session persist failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not start a session")
		return
	}
	if len(created.ReplacedHash) > 0 {
		_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(created.ReplacedHash)))
	}
	s.retireSupersededDevices(r.Context(), created.Superseded)
	// Recorded after the session exists, so a build history can only describe a
	// television that got as far as signing in.
	if err := s.store.RecordDeviceVersion(r.Context(), sess.DeviceID, sess.ClientVersion); err != nil {
		s.loggerFor(r.Context()).Warn("device version record failed",
			"device_id", sess.DeviceID, "error", err)
	}
	if s.forYou != nil {
		s.forYou.MarkDirty(r.Context(), sess)
		s.forYou.RefreshAsync(sess, false)
	}

	// Now that the session exists, the request line this call ends with can name it too.
	identify(r.Context(), sess)
	s.loggerFor(r.Context()).Info("signed in",
		"emby_user", sess.EmbyUserID,
		"device_id", sess.DeviceID,
		"protocol", clientLogValue(sess.ClientProtocol),
		"replaced_session", len(created.ReplacedHash) > 0,
	)

	writeJSON(w, http.StatusOK, loginResponse{
		Token: token, UserID: sess.EmbyUserID, Username: sess.Username, ServerID: sess.ServerID,
	})
}

func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request, sess store.Session) {
	if err := s.store.DeleteSession(r.Context(), sess.TokenHash); err != nil {
		s.log.Error("session delete failed", "error", err)
	}
	_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(sess.TokenHash)))
	_ = s.cache.InvalidateUser(r.Context(), sess.EmbyUserID)
	s.loggerFor(r.Context()).Info("signed out", "device_id", sess.DeviceID)
	w.WriteHeader(http.StatusNoContent)
}

// handleSession lets the TV confirm a stored token is still good before rendering.
func (s *Server) handleSession(w http.ResponseWriter, _ *http.Request, sess store.Session) {
	writeJSON(w, http.StatusOK, loginResponse{
		UserID: sess.EmbyUserID, Username: sess.Username, ServerID: sess.ServerID,
	})
}

func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request, current store.Session) {
	sessions, err := s.store.SessionsForUser(r.Context(), current.EmbyUserID)
	if err != nil {
		s.log.Error("device list failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not list devices")
		return
	}
	devices := make([]deviceSessionResponse, 0, len(sessions))
	for _, sess := range sessions {
		devices = append(devices, deviceSessionResponse{
			DeviceID: sess.DeviceID, DeviceName: sess.DeviceName,
			ClientVersion: sess.ClientVersion, LastSeenAt: sess.LastSeenAt,
			Current: string(sess.TokenHash) == string(current.TokenHash),
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"devices": devices})
}

func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request, current store.Session) {
	deviceID := strings.TrimSpace(r.PathValue("deviceID"))
	if deviceID == "" {
		writeError(w, http.StatusBadRequest, "device id is required")
		return
	}
	if deviceID == current.DeviceID {
		writeError(w, http.StatusBadRequest, "sign out to remove the current device")
		return
	}
	tokenHash, err := s.store.DeleteUserDevice(r.Context(), current.EmbyUserID, deviceID)
	if err == store.ErrNotFound {
		writeError(w, http.StatusNotFound, "device not found")
		return
	}
	if err != nil {
		s.log.Error("device revoke failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not remove device")
		return
	}
	_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(tokenHash)))
	s.retireEmbyDevice(r.Context(), deviceID)
	if err := s.store.DeleteDeviceVersions(r.Context(), deviceID); err != nil {
		s.loggerFor(r.Context()).Warn("device version cleanup failed",
			"removed_device_id", deviceID, "error", err)
	}
	// A device disappearing from a household is worth a line: the next thing that TV
	// reports is a sign-in, and the two together explain each other.
	s.loggerFor(r.Context()).Info("device signed out remotely", "removed_device_id", deviceID)
	w.WriteHeader(http.StatusNoContent)
}

// retireSupersededDevices finishes what CreateSession started: the rows for a television
// under a device id it no longer uses are already gone from Postgres, and this takes the
// rest of that identity with them — the cached session, the build history and the record
// Emby is still holding in its own devices list.
//
// Best-effort throughout, and deliberately after the sign-in has succeeded: tidying up a
// set's previous life must never be what stops it getting in.
func (s *Server) retireSupersededDevices(ctx context.Context, devices []store.SupersededDevice) {
	if len(devices) == 0 {
		return
	}
	ids := make([]string, 0, len(devices))
	for _, device := range devices {
		ids = append(ids, device.DeviceID)
		if s.cache != nil && len(device.TokenHash) > 0 {
			_ = s.cache.Delete(ctx, cache.SessionKey(hexHash(device.TokenHash)))
		}
		s.retireEmbyDevice(ctx, device.DeviceID)
	}
	if err := s.store.DeleteDeviceVersions(ctx, ids...); err != nil {
		s.loggerFor(ctx).Warn("device version cleanup failed", "error", err)
	}
	s.loggerFor(ctx).Info("device identity superseded", "retired_device_ids", ids)
}

// retireEmbyDevice deletes the Emby device record a removed television left behind.
//
// Revoking the gateway session only takes the TV out of Settings → Devices; Emby keeps
// its own record until the record itself is deleted, so without this a set removed from
// one list stays visible in the other. Deliberately best-effort: the session is already
// gone, which is what actually ends that TV's access, and a lingering Emby row is not
// worth failing the request the operator made. It needs the sync credentials because a
// device record belongs to Emby's server, not to the viewer whose session was removed.
func (s *Server) retireEmbyDevice(ctx context.Context, deviceID string) {
	if s.emby == nil || s.cfg.SyncAPIKey == "" || deviceID == "" {
		return
	}
	if err := s.emby.DeleteDevice(ctx, emby.Credentials{
		UserID: s.cfg.SyncUserID, Token: s.cfg.SyncAPIKey,
		DeviceID: "memby-gateway", DeviceName: "Memby Gateway",
	}, deviceID); err != nil {
		s.loggerFor(ctx).Warn("emby device cleanup failed",
			"removed_device_id", deviceID, "error", err)
	}
}

func (s *Server) handleRenameDevice(w http.ResponseWriter, r *http.Request, current store.Session) {
	deviceID := strings.TrimSpace(r.PathValue("deviceID"))
	var req renameDeviceRequest
	if deviceID == "" || json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<10)).Decode(&req) != nil {
		writeError(w, http.StatusBadRequest, "device id and name are required")
		return
	}
	req.DeviceName = strings.TrimSpace(req.DeviceName)
	if req.DeviceName == "" {
		writeError(w, http.StatusBadRequest, "device name is required")
		return
	}
	if len([]rune(req.DeviceName)) > 80 {
		writeError(w, http.StatusBadRequest, "device name is too long")
		return
	}
	if err := s.store.RenameUserDevice(r.Context(), current.EmbyUserID, deviceID, req.DeviceName); err == store.ErrNotFound {
		writeError(w, http.StatusNotFound, "device not found")
		return
	} else if err != nil {
		s.log.Error("device rename failed", "error", err)
		writeError(w, http.StatusInternalServerError, "could not rename device")
		return
	}
	s.loggerFor(r.Context()).Info("device renamed",
		"renamed_device_id", deviceID, "new_name", req.DeviceName,
	)
	w.WriteHeader(http.StatusNoContent)
}
