package infra

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"
)

// SynapseAdminClient wraps the Synapse Admin API (/_synapse/admin/...) for user and room management.
type SynapseAdminClient struct {
	HomeserverURL string       // e.g. https://matrix-af2f3d.organic-machine.com
	AdminToken    string       // access_token of a user with admin:true in Synapse
	HTTPClient    *http.Client // optional; default 30s timeout
}

// NewSynapseAdminClient creates a client with sensible defaults.
func NewSynapseAdminClient(homeserver, adminToken string) *SynapseAdminClient {
	return &SynapseAdminClient{
		HomeserverURL: homeserver,
		AdminToken:    adminToken,
		HTTPClient:    &http.Client{Timeout: 30 * time.Second},
	}
}

// AdminUser represents a Synapse user as returned by the admin API.
type AdminUser struct {
	UserID      string `json:"name"`
	DisplayName string `json:"displayname"`
	AvatarURL   string `json:"avatar_url"`
	Admin       bool   `json:"admin"`
	Deactivated bool   `json:"deactivated"`
	IsGuest     bool   `json:"is_guest"`
	CreationTs  int64  `json:"creation_ts"`
	LastSeenTs  int64  `json:"last_seen_ts"`
}

// ListUsersFilter controls pagination and filtering for ListUsers.
type ListUsersFilter struct {
	From        int    // pagination offset
	Limit       int    // default 100
	SearchTerm  string // filter by name / user_id
	Deactivated *bool  // nil = both, true/false to filter
	Admins      *bool  // nil = both, true/false to filter
}

// ListUsersResult holds a page of users plus pagination metadata.
type ListUsersResult struct {
	Users      []AdminUser
	TotalCount int
	NextToken  *int // nil if last page
}

// AdminRoom represents a Synapse room as returned by the admin API.
type AdminRoom struct {
	RoomID         string `json:"room_id"`
	Name           string `json:"name"`
	CanonicalAlias string `json:"canonical_alias"`
	JoinedMembers  int    `json:"joined_members"`
	JoinedLocal    int    `json:"joined_local_members"`
	Version        string `json:"version"`
	Encrypted      bool   `json:"encryption_enabled"`
	Federatable    bool   `json:"federatable"`
	Public         bool   `json:"public"`
}

// AdminDevice represents a device belonging to a Synapse user.
type AdminDevice struct {
	DeviceID    string `json:"device_id"`
	DisplayName string `json:"display_name"`
	LastSeenIP  string `json:"last_seen_ip"`
	LastSeenTs  int64  `json:"last_seen_ts"`
}

// synapseError is the error envelope returned by Synapse for 4xx/5xx responses.
type synapseError struct {
	ErrCode string `json:"errcode"`
	ErrMsg  string `json:"error"`
}

// client returns the HTTPClient, falling back to a 30-second default.
func (c *SynapseAdminClient) client() *http.Client {
	if c.HTTPClient != nil {
		return c.HTTPClient
	}
	return &http.Client{Timeout: 30 * time.Second}
}

// do executes an authenticated request and returns the raw response body.
// Returns an error for HTTP >= 400, including the Synapse errcode when present.
func (c *SynapseAdminClient) do(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
	var bodyReader io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("synapse_admin: marshal request body: %w", err)
		}
		bodyReader = bytes.NewReader(b)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.HomeserverURL+path, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("synapse_admin: build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.AdminToken)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.client().Do(req)
	if err != nil {
		return nil, fmt.Errorf("synapse_admin: http %s %s: %w", method, path, err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("synapse_admin: read response: %w", err)
	}

	if resp.StatusCode >= 500 {
		var se synapseError
		if jsonErr := json.Unmarshal(data, &se); jsonErr == nil && se.ErrCode != "" {
			return nil, fmt.Errorf("synapse_admin: synapse internal %d %s: %s", resp.StatusCode, se.ErrCode, se.ErrMsg)
		}
		return nil, fmt.Errorf("synapse_admin: synapse internal: %d", resp.StatusCode)
	}

	if resp.StatusCode >= 400 {
		var se synapseError
		if jsonErr := json.Unmarshal(data, &se); jsonErr == nil && se.ErrCode != "" {
			return nil, fmt.Errorf("synapse_admin: %s %s → %d %s: %s", method, path, resp.StatusCode, se.ErrCode, se.ErrMsg)
		}
		return nil, fmt.Errorf("synapse_admin: %s %s → HTTP %d", method, path, resp.StatusCode)
	}

	return data, nil
}

// --- Users ---

// ListUsers returns a page of users matching the given filter.
// Use ListUsersResult.NextToken to paginate: set ListUsersFilter.From = *NextToken on the next call.
func (c *SynapseAdminClient) ListUsers(ctx context.Context, f ListUsersFilter) (*ListUsersResult, error) {
	limit := f.Limit
	if limit <= 0 {
		limit = 100
	}

	q := url.Values{}
	q.Set("from", strconv.Itoa(f.From))
	q.Set("limit", strconv.Itoa(limit))
	if f.SearchTerm != "" {
		q.Set("user_id", f.SearchTerm)
	}
	if f.Deactivated != nil {
		q.Set("deactivated", strconv.FormatBool(*f.Deactivated))
	}
	if f.Admins != nil {
		q.Set("admins", strconv.FormatBool(*f.Admins))
	}

	path := "/_synapse/admin/v2/users?" + q.Encode()
	data, err := c.do(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	var raw struct {
		Users     []AdminUser `json:"users"`
		Total     int         `json:"total"`
		NextToken *int        `json:"next_token"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("synapse_admin: ListUsers decode: %w", err)
	}
	return &ListUsersResult{
		Users:      raw.Users,
		TotalCount: raw.Total,
		NextToken:  raw.NextToken,
	}, nil
}

// GetUser returns the admin view of a single user by their full Matrix ID (e.g. @user:server).
func (c *SynapseAdminClient) GetUser(ctx context.Context, userID string) (*AdminUser, error) {
	path := "/_synapse/admin/v2/users/" + url.PathEscape(userID)
	data, err := c.do(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	var u AdminUser
	if err := json.Unmarshal(data, &u); err != nil {
		return nil, fmt.Errorf("synapse_admin: GetUser decode: %w", err)
	}
	return &u, nil
}

// DeactivateUser deactivates a user account.
// If erase=true, Synapse purges all user data — IRREVERSIBLE.
func (c *SynapseAdminClient) DeactivateUser(ctx context.Context, userID string, erase bool) error {
	path := "/_synapse/admin/v1/deactivate/" + url.PathEscape(userID)
	_, err := c.do(ctx, http.MethodPost, path, map[string]bool{"erase": erase})
	return err
}

// ResetPassword sets a new password for the given user.
// If logoutDevices=true, all existing sessions are invalidated.
func (c *SynapseAdminClient) ResetPassword(ctx context.Context, userID, newPassword string, logoutDevices bool) error {
	path := "/_synapse/admin/v1/reset_password/" + url.PathEscape(userID)
	body := map[string]interface{}{
		"new_password":   newPassword,
		"logout_devices": logoutDevices,
	}
	_, err := c.do(ctx, http.MethodPost, path, body)
	return err
}

// --- Rooms ---

// ListRooms returns a page of rooms.
// from and limit control pagination; searchTerm filters by room name/alias.
func (c *SynapseAdminClient) ListRooms(ctx context.Context, from, limit int, searchTerm string) (rooms []AdminRoom, total int, nextToken *int, err error) {
	if limit <= 0 {
		limit = 100
	}

	q := url.Values{}
	q.Set("from", strconv.Itoa(from))
	q.Set("limit", strconv.Itoa(limit))
	q.Set("order_by", "name")
	if searchTerm != "" {
		q.Set("search_term", searchTerm)
	}

	path := "/_synapse/admin/v1/rooms?" + q.Encode()
	data, err := c.do(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, 0, nil, err
	}

	var raw struct {
		Rooms      []AdminRoom `json:"rooms"`
		TotalRooms int         `json:"total_rooms"`
		NextBatch  *int        `json:"next_batch"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, 0, nil, fmt.Errorf("synapse_admin: ListRooms decode: %w", err)
	}
	return raw.Rooms, raw.TotalRooms, raw.NextBatch, nil
}

// GetRoom returns the admin view of a single room by its room ID (e.g. !room:server).
func (c *SynapseAdminClient) GetRoom(ctx context.Context, roomID string) (*AdminRoom, error) {
	path := "/_synapse/admin/v1/rooms/" + url.PathEscape(roomID)
	data, err := c.do(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	var r AdminRoom
	if err := json.Unmarshal(data, &r); err != nil {
		return nil, fmt.Errorf("synapse_admin: GetRoom decode: %w", err)
	}
	return &r, nil
}

// DeleteRoom schedules an async room deletion. Returns the delete_id for status polling.
// purge=true destroys all messages and state (IRREVERSIBLE).
// block=true prevents new users from joining after deletion.
func (c *SynapseAdminClient) DeleteRoom(ctx context.Context, roomID, reason string, purge, block bool) (deleteID string, err error) {
	path := "/_synapse/admin/v2/rooms/" + url.PathEscape(roomID)
	body := map[string]interface{}{
		"new_room_user_id": nil,
		"purge":            purge,
		"block":            block,
		"message":          reason,
	}
	data, err := c.do(ctx, http.MethodDelete, path, body)
	if err != nil {
		return "", err
	}

	var raw struct {
		DeleteID string `json:"delete_id"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return "", fmt.Errorf("synapse_admin: DeleteRoom decode: %w", err)
	}
	return raw.DeleteID, nil
}

// --- Devices ---

// ListUserDevices returns all devices registered for the given user.
func (c *SynapseAdminClient) ListUserDevices(ctx context.Context, userID string) ([]AdminDevice, error) {
	path := "/_synapse/admin/v2/users/" + url.PathEscape(userID) + "/devices"
	data, err := c.do(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	var raw struct {
		Devices []AdminDevice `json:"devices"`
		Total   int           `json:"total"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("synapse_admin: ListUserDevices decode: %w", err)
	}
	return raw.Devices, nil
}

// DeleteUserDevice removes a specific device from a user's account.
func (c *SynapseAdminClient) DeleteUserDevice(ctx context.Context, userID, deviceID string) error {
	path := "/_synapse/admin/v2/users/" + url.PathEscape(userID) + "/devices/" + url.PathEscape(deviceID)
	_, err := c.do(ctx, http.MethodDelete, path, nil)
	return err
}
