package infra

import (
	"context"
	"fmt"
	"log"
	"sort"
	"strings"

	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

// RoomSummary es el resumen de una room Matrix para renderizar en el sidebar de un cliente.
type RoomSummary struct {
	RoomID         string   `json:"room_id"`
	Name           string   `json:"name,omitempty"`            // m.room.name o fallback
	CanonicalAlias string   `json:"canonical_alias,omitempty"` // #room:server
	AvatarMxc      string   `json:"avatar_mxc,omitempty"`      // mxc://...
	Topic          string   `json:"topic,omitempty"`
	IsDirect       bool     `json:"is_direct"`    // m.direct account_data
	IsSpace        bool     `json:"is_space"`     // m.room.type == m.space
	IsEncrypted    bool     `json:"is_encrypted"` // m.room.encryption state event presente
	MemberCount    int      `json:"member_count"`
	LastEventTs    int64    `json:"last_event_ts"` // unix ms del ultimo evento conocido
	UnreadCount    int      `json:"unread_count"`  // notifications.unread + highlight
	Tags           []string `json:"tags,omitempty"` // m.tag account_data
}

// MatrixRoomListConfig agrupa los parametros de MatrixRoomList.
type MatrixRoomListConfig struct {
	Client *mautrix.Client
}

// MatrixRoomList devuelve todos los rooms en los que el usuario esta unido,
// ordenados por LastEventTs DESC (recientes primero).
//
// Estrategia:
//  1. JoinedRooms() para la lista de room IDs.
//  2. m.direct account_data para detectar DMs.
//  3. Para cada room: State() -> nombre, alias, topic, avatar, encryption, space, members.
//  4. Messages(limit=1) -> LastEventTs (TODO: coste N*HTTP; cachear con TTL 30s).
//  5. GetRoomAccountData("m.tag") -> Tags.
//
// Sub-operaciones que fallan por room concreto no abortan el global.
// LastEventTs puede ser 0 si el store no lo cachea (ver ## Gotchas del .md).
func MatrixRoomList(ctx context.Context, cfg MatrixRoomListConfig) ([]RoomSummary, error) {
	if cfg.Client == nil {
		return nil, fmt.Errorf("matrix_room_list: client no puede ser nil")
	}
	client := cfg.Client

	// 1. Rooms unidos
	respJoined, err := client.JoinedRooms(ctx)
	if err != nil {
		return nil, fmt.Errorf("matrix_room_list: JoinedRooms: %w", err)
	}
	if len(respJoined.JoinedRooms) == 0 {
		return []RoomSummary{}, nil
	}

	// 2. m.direct -> set roomID -> true
	directSet := loadDirectRooms(ctx, client)

	// 3. Construir summaries (secuencial para v0.1.0)
	results := make([]RoomSummary, 0, len(respJoined.JoinedRooms))
	for _, roomID := range respJoined.JoinedRooms {
		s := buildRoomSummaryFromState(ctx, client, roomID, directSet)
		results = append(results, s)
	}

	// 4. Ordenar DESC por LastEventTs; si empatan (ej. todo 0) -> alfabetico por Name
	sort.Slice(results, func(i, j int) bool {
		if results[i].LastEventTs != results[j].LastEventTs {
			return results[i].LastEventTs > results[j].LastEventTs
		}
		return results[i].Name < results[j].Name
	})

	return results, nil
}

// loadDirectRooms carga m.direct account_data y devuelve un set roomID -> true.
// Falla silenciosamente: si hay error devuelve mapa vacio (IsDirect quedara false).
func loadDirectRooms(ctx context.Context, client *mautrix.Client) map[id.RoomID]bool {
	result := make(map[id.RoomID]bool)
	var directContent event.DirectChatsEventContent
	if err := client.GetAccountData(ctx, "m.direct", &directContent); err != nil {
		log.Printf("matrix_room_list: GetAccountData(m.direct) warning: %v", err)
		return result
	}
	for _, rooms := range directContent {
		for _, rid := range rooms {
			result[rid] = true
		}
	}
	return result
}

// buildRoomSummaryFromState construye el RoomSummary para un room concreto.
// Si State() falla usa el roomID como Name de emergencia.
func buildRoomSummaryFromState(ctx context.Context, client *mautrix.Client, roomID id.RoomID, directSet map[id.RoomID]bool) RoomSummary {
	s := RoomSummary{
		RoomID:   string(roomID),
		IsDirect: directSet[roomID],
	}

	// State del room
	stateMap, err := client.State(ctx, roomID)
	if err != nil {
		log.Printf("matrix_room_list: State(%s) warning: %v", roomID, err)
		s.Name = deriveRoomName(&s, nil)
		return s
	}

	fillStateFields(&s, stateMap)
	s.Name = deriveRoomName(&s, stateMap)

	// Tags: m.tag room account_data
	s.Tags = loadRoomTags(ctx, client, roomID)

	// LastEventTs: Messages(limit=1, dir=backward)
	// TODO(0148): caro N*HTTP -> cachear en backend con TTL 30s.
	msgs, err := client.Messages(ctx, roomID, "", "", mautrix.DirectionBackward, nil, 1)
	if err != nil {
		log.Printf("matrix_room_list: Messages(%s) warning: %v", roomID, err)
		// No fatal: LastEventTs queda 0 y el room cae al fondo del orden
	} else if msgs != nil && len(msgs.Chunk) > 0 {
		s.LastEventTs = msgs.Chunk[0].Timestamp
	}

	return s
}

// ensureParsed llama ParseRaw si el contenido no esta aun parseado.
// ParseRaw devuelve ErrContentAlreadyParsed cuando ya fue parseado (p.ej.
// por parseRoomStateArray al deserializar el state); en ese caso ignoramos
// el error y usamos el Parsed existente.
func ensureParsed(c *event.Content, evtType event.Type) {
	if c.Parsed == nil {
		_ = c.ParseRaw(evtType)
	}
}

// fillStateFields rellena los campos del RoomSummary a partir del state map.
// parseRoomStateArray ya llama ParseRaw al deserializar, por lo que es posible
// que Content.Parsed este ya populado. ensureParsed maneja ambos casos.
func fillStateFields(s *RoomSummary, stateMap mautrix.RoomStateMap) {
	// m.room.name
	if nameEvts, ok := stateMap[event.StateRoomName]; ok {
		if nameEvt, ok := nameEvts[""]; ok {
			ensureParsed(&nameEvt.Content, event.StateRoomName)
			if c := nameEvt.Content.AsRoomName(); c != nil {
				s.Name = c.Name
			}
		}
	}

	// m.room.canonical_alias
	if aliasEvts, ok := stateMap[event.StateCanonicalAlias]; ok {
		if aliasEvt, ok := aliasEvts[""]; ok {
			ensureParsed(&aliasEvt.Content, event.StateCanonicalAlias)
			if c := aliasEvt.Content.AsCanonicalAlias(); c != nil {
				s.CanonicalAlias = string(c.Alias)
			}
		}
	}

	// m.room.avatar
	if avatarEvts, ok := stateMap[event.StateRoomAvatar]; ok {
		if avatarEvt, ok := avatarEvts[""]; ok {
			ensureParsed(&avatarEvt.Content, event.StateRoomAvatar)
			if c := avatarEvt.Content.AsRoomAvatar(); c != nil {
				s.AvatarMxc = string(c.URL)
			}
		}
	}

	// m.room.topic
	if topicEvts, ok := stateMap[event.StateTopic]; ok {
		if topicEvt, ok := topicEvts[""]; ok {
			ensureParsed(&topicEvt.Content, event.StateTopic)
			if c := topicEvt.Content.AsTopic(); c != nil {
				s.Topic = c.Topic
			}
		}
	}

	// m.room.encryption (existence = encrypted)
	if encEvts, ok := stateMap[event.StateEncryption]; ok {
		if _, ok := encEvts[""]; ok {
			s.IsEncrypted = true
		}
	}

	// m.room.create -> IsSpace si type == "m.space"
	if createEvts, ok := stateMap[event.StateCreate]; ok {
		if createEvt, ok := createEvts[""]; ok {
			ensureParsed(&createEvt.Content, event.StateCreate)
			if c := createEvt.Content.AsCreate(); c != nil {
				s.IsSpace = c.Type == event.RoomTypeSpace
			}
		}
	}

	// m.room.member: contar membership == join
	if memberEvts, ok := stateMap[event.StateMember]; ok {
		count := 0
		for _, memberEvt := range memberEvts {
			ensureParsed(&memberEvt.Content, event.StateMember)
			if c := memberEvt.Content.AsMember(); c != nil && c.Membership == event.MembershipJoin {
				count++
			}
		}
		s.MemberCount = count
	}
}

// deriveRoomName calcula el nombre display para el room siguiendo la jerarquia:
//  1. Name (ya seteado desde m.room.name).
//  2. CanonicalAlias.
//  3. "Direct Message" si IsDirect.
//  4. Lista de otros miembros si los hay (max 3).
//  5. "Empty room" si MemberCount <= 1.
func deriveRoomName(s *RoomSummary, stateMap mautrix.RoomStateMap) string {
	if s.Name != "" {
		return s.Name
	}
	if s.CanonicalAlias != "" {
		return s.CanonicalAlias
	}
	if s.IsDirect {
		// Intentar obtener displayname del otro miembro desde el state
		if stateMap != nil {
			if memberEvts, ok := stateMap[event.StateMember]; ok {
				for userKey, memberEvt := range memberEvts {
					ensureParsed(&memberEvt.Content, event.StateMember)
					if c := memberEvt.Content.AsMember(); c != nil &&
						c.Membership == event.MembershipJoin &&
						userKey != "" {
						if c.Displayname != "" {
							return c.Displayname
						}
						return userKey // user ID como fallback
					}
				}
			}
		}
		return "Direct Message"
	}
	if stateMap != nil && s.MemberCount > 1 {
		// Lista de displaynames de otros miembros (max 3)
		names := collectMemberNames(stateMap, 3)
		if len(names) > 0 {
			return strings.Join(names, ", ")
		}
	}
	return "Empty room"
}

// collectMemberNames extrae hasta maxN displaynames de joined members del state.
func collectMemberNames(stateMap mautrix.RoomStateMap, maxN int) []string {
	names := make([]string, 0, maxN)
	if memberEvts, ok := stateMap[event.StateMember]; ok {
		for userKey, memberEvt := range memberEvts {
			if len(names) >= maxN {
				break
			}
			ensureParsed(&memberEvt.Content, event.StateMember)
			if c := memberEvt.Content.AsMember(); c != nil && c.Membership == event.MembershipJoin {
				if c.Displayname != "" {
					names = append(names, c.Displayname)
				} else if userKey != "" {
					names = append(names, userKey)
				}
			}
		}
	}
	return names
}

// loadRoomTags carga m.tag room account_data y devuelve los tag names como []string.
// Falla silenciosamente devolviendo nil.
func loadRoomTags(ctx context.Context, client *mautrix.Client, roomID id.RoomID) []string {
	var tagContent event.TagEventContent
	if err := client.GetRoomAccountData(ctx, roomID, "m.tag", &tagContent); err != nil {
		// No fatal: rooms sin tags dan 404, lo cual es normal
		return nil
	}
	if len(tagContent.Tags) == 0 {
		return nil
	}
	tags := make([]string, 0, len(tagContent.Tags))
	for tag := range tagContent.Tags {
		tags = append(tags, string(tag))
	}
	sort.Strings(tags) // orden determinista
	return tags
}
