package state

import (
	"encoding/json"
	"fmt"
	"slices"
	"strings"
	"sync"
)

const (
	maxActivityLogEntries   = 400
	adminLogBroadcastLimit  = 200
)

type Manager struct {
	mu    sync.RWMutex
	rooms map[string]*Room
	store *DiskStore
}

func NewManager(dataPath string) (*Manager, error) {
	store, err := NewDiskStore(dataPath)
	if err != nil {
		return nil, err
	}

	manager := &Manager{
		rooms: make(map[string]*Room),
		store: store,
	}

	if loadErr := manager.loadFromDisk(); loadErr != nil {
		return nil, loadErr
	}

	return manager, nil
}

func (m *Manager) CreateRoom(input CreateRoomInput) (CreateRoomResult, error) {
	roomName := normalizeName(input.RoomName, 80)
	creatorUsername := normalizeName(input.CreatorUsername, 32)
	if roomName == "" {
		roomName = "Scrum Poker Room"
	}
	if creatorUsername == "" {
		creatorUsername = "host"
	}

	maxPeople := input.MaxPeople
	if maxPeople < 2 {
		maxPeople = 2
	}
	if maxPeople > 50 {
		maxPeople = 50
	}

	revealMode := input.RevealMode
	if revealMode != RevealModeManual && revealMode != RevealModeAutoAll {
		revealMode = RevealModeManual
	}

	cards := make([]string, 0, len(input.Cards))
	for _, rawCard := range input.Cards {
		card := normalizeCard(rawCard)
		if card == "" {
			continue
		}
		cards = append(cards, card)
	}
	if len(cards) == 0 {
		cards = []string{"0", "1", "2", "3", "5", "8", "13", "21", "?"}
	}

	roomID := newUUIDv4()
	adminToken := randomHex(24)
	creatorID := newUUIDv4()
	now := nowUTC()
	allowVoteChange := true
	if input.AllowVoteChange != nil {
		allowVoteChange = *input.AllowVoteChange
	}

	settings := RoomSettings{
		RoomName:         roomName,
		MaxPeople:        maxPeople,
		Cards:            cards,
		AllowSpectators:  input.AllowSpectators,
		AnonymousVoting:  input.AnonymousVoting,
		AutoReset:        input.AutoReset,
		AllowVoteChange:  allowVoteChange,
		RevealMode:       revealMode,
		VotingTimeoutSec: max(0, input.VotingTimeoutSec),
	}

	password := strings.TrimSpace(input.Password)
	if password != "" {
		settings.PasswordSalt = randomHex(16)
		settings.PasswordHash = hashPassword(password, settings.PasswordSalt)
	}

	creator := &Participant{
		ID:           creatorID,
		SessionToken: randomHex(24),
		Username:  creatorUsername,
		Role:      RoleParticipant,
		IsAdmin:   true,
		Connected: true,
		HasVoted:  false,
		JoinedAt:  now,
		UpdatedAt: now,
	}

	room := &Room{
		ID:         roomID,
		AdminToken: adminToken,
		CreatedAt:  now,
		UpdatedAt:  now,
		Settings:   settings,
		Round: RoundState{
			Revealed: false,
		},
		Participants: map[string]*Participant{
			creatorID: creator,
		},
		ActivityLog: []ActivityLogEntry{},
		subscribers: map[string]*subscriber{},
	}
	m.appendActivityLogLocked(room, "%s created the room as admin.", creator.Username)

	m.mu.Lock()
	m.rooms[roomID] = room
	m.mu.Unlock()

	room.mu.Lock()
	if err := m.store.Save(room); err != nil {
		room.mu.Unlock()
		m.mu.Lock()
		delete(m.rooms, roomID)
		m.mu.Unlock()
		return CreateRoomResult{}, err
	}
	room.mu.Unlock()

	result := CreateRoomResult{
		RoomID:               roomID,
		CreatorParticipantID: creatorID,
		CreatorSessionToken:  creator.SessionToken,
		AdminToken:           adminToken,
		ParticipantLink:      "/room/" + roomID,
		AdminLink:            "/room/" + roomID + "?adminToken=" + adminToken,
	}

	return result, nil
}

func (m *Manager) JoinRoom(roomID string, input JoinRoomInput) (JoinRoomResult, error) {
	room, err := m.getRoom(roomID)
	if err != nil {
		return JoinRoomResult{}, err
	}

	room.mu.Lock()
	defer room.mu.Unlock()

	username := normalizeName(input.Username, 32)
	if username == "" {
		username = "anonymous"
	}

	role := input.Role
	if role == "" {
		role = RoleParticipant
	}
	if role != RoleParticipant && role != RoleViewer {
		return JoinRoomResult{}, ErrInvalidRole
	}

	now := nowUTC()
	isAdminByToken := input.AdminToken != "" && input.AdminToken == room.AdminToken

	if room.Settings.PasswordHash != "" && input.ParticipantID == "" {
		if !passwordMatches(input.Password, room.Settings.PasswordSalt, room.Settings.PasswordHash) {
			return JoinRoomResult{}, ErrPasswordRequired
		}
	}

	if input.ParticipantID != "" {
		existing, ok := room.Participants[input.ParticipantID]
		if !ok {
			return JoinRoomResult{}, ErrParticipantNotFound
		}
		if !secureTokenMatches(existing.SessionToken, input.SessionToken) {
			return JoinRoomResult{}, ErrUnauthorized
		}

		wasConnected := existing.Connected
		existing.Username = username
		existing.Connected = true
		existing.UpdatedAt = now
		if isAdminByToken {
			existing.IsAdmin = true
		}
		if !wasConnected {
			m.appendActivityLogLocked(room, "%s joined as %s.", existing.Username, existing.Role)
		}

		room.UpdatedAt = now
		if err := m.store.Save(room); err != nil {
			return JoinRoomResult{}, err
		}

		go m.broadcastRoom(room.ID)
		return JoinRoomResult{
			ParticipantID: existing.ID,
			SessionToken:  existing.SessionToken,
			IsAdmin:       existing.IsAdmin,
			Role:          existing.Role,
			Username:      existing.Username,
		}, nil
	}

	if role == RoleViewer && !room.Settings.AllowSpectators {
		return JoinRoomResult{}, ErrSpectatorsBlocked
	}

	if role == RoleParticipant {
		count := 0
		for _, participant := range room.Participants {
			if participant.Role == RoleParticipant {
				count++
			}
		}
		if count >= room.Settings.MaxPeople {
			return JoinRoomResult{}, ErrRoomFull
		}
	}

	participant := &Participant{
		ID:           newUUIDv4(),
		SessionToken: randomHex(24),
		Username:  username,
		Role:      role,
		IsAdmin:   isAdminByToken,
		Connected: true,
		HasVoted:  false,
		JoinedAt:  now,
		UpdatedAt: now,
	}

	room.Participants[participant.ID] = participant
	m.appendActivityLogLocked(room, "%s joined as %s.", participant.Username, participant.Role)
	room.UpdatedAt = now

	if err := m.store.Save(room); err != nil {
		return JoinRoomResult{}, err
	}

	go m.broadcastRoom(room.ID)
	return JoinRoomResult{
		ParticipantID: participant.ID,
		SessionToken:  participant.SessionToken,
		IsAdmin:       participant.IsAdmin,
		Role:          participant.Role,
		Username:      participant.Username,
	}, nil
}

func (m *Manager) LeaveRoom(roomID, participantID, sessionToken string) error {
	room, err := m.getRoom(roomID)
	if err != nil {
		return err
	}

	room.mu.Lock()
	defer room.mu.Unlock()

	participant, err := m.authorizeParticipantLocked(room, participantID, sessionToken)
	if err != nil {
		return err
	}

	if !participant.Connected {
		return nil
	}
	m.disconnectParticipantLocked(room, participant)
	m.appendActivityLogLocked(room, "%s left the room.", participant.Username)

	if err := m.store.Save(room); err != nil {
		return err
	}

	go m.broadcastRoom(room.ID)
	return nil
}

func (m *Manager) CastVote(roomID, participantID, sessionToken, card string) error {
	room, err := m.getRoom(roomID)
	if err != nil {
		return err
	}

	room.mu.Lock()
	defer room.mu.Unlock()

	participant, err := m.authorizeParticipantLocked(room, participantID, sessionToken)
	if err != nil {
		return err
	}
	if participant.Role != RoleParticipant {
		return ErrUnauthorized
	}

	normalizedCard := normalizeCard(card)
	if normalizedCard == "" || !slices.Contains(room.Settings.Cards, normalizedCard) {
		return ErrInvalidCard
	}

	if participant.HasVoted {
		if participant.VoteValue == normalizedCard {
			return nil
		}
		if !room.Settings.AllowVoteChange {
			return ErrVoteChangeLocked
		}
	}

	previousVote := participant.VoteValue
	hadVoted := participant.HasVoted
	participant.HasVoted = true
	participant.VoteValue = normalizedCard
	participant.UpdatedAt = nowUTC()
	room.UpdatedAt = nowUTC()
	if hadVoted {
		m.appendActivityLogLocked(room, "%s changed vote from %s to %s.", participant.Username, previousVote, normalizedCard)
	} else {
		m.appendActivityLogLocked(room, "%s voted %s.", participant.Username, normalizedCard)
	}

	if room.Settings.RevealMode == RevealModeAutoAll && allActiveParticipantsVoted(room) {
		room.Round.Revealed = true
		m.appendActivityLogLocked(room, "Votes auto-revealed after all active participants voted.")
	}

	if err := m.store.Save(room); err != nil {
		return err
	}

	go m.broadcastRoom(room.ID)
	return nil
}

func (m *Manager) RevealVotes(roomID, participantID, sessionToken string) error {
	room, err := m.getRoom(roomID)
	if err != nil {
		return err
	}

	room.mu.Lock()
	defer room.mu.Unlock()

	participant, err := m.authorizeParticipantLocked(room, participantID, sessionToken)
	if err != nil {
		return err
	}
	if !participant.IsAdmin {
		return ErrUnauthorized
	}

	room.Round.Revealed = true
	room.UpdatedAt = nowUTC()
	m.appendActivityLogLocked(room, "%s revealed the votes.", participant.Username)

	if err := m.store.Save(room); err != nil {
		return err
	}

	go m.broadcastRoom(room.ID)
	return nil
}

func (m *Manager) ResetVotes(roomID, participantID, sessionToken string) error {
	room, err := m.getRoom(roomID)
	if err != nil {
		return err
	}

	room.mu.Lock()
	defer room.mu.Unlock()

	participant, err := m.authorizeParticipantLocked(room, participantID, sessionToken)
	if err != nil {
		return err
	}
	if !participant.IsAdmin {
		return ErrUnauthorized
	}

	m.resetVotesLocked(room)
	room.UpdatedAt = nowUTC()
	m.appendActivityLogLocked(room, "%s reset all votes.", participant.Username)

	if err := m.store.Save(room); err != nil {
		return err
	}

	go m.broadcastRoom(room.ID)
	return nil
}

func (m *Manager) Subscribe(roomID, participantID, sessionToken string) (<-chan []byte, []byte, func(), error) {
	room, err := m.getRoom(roomID)
	if err != nil {
		return nil, nil, nil, err
	}

	room.mu.Lock()
	participant, authErr := m.authorizeParticipantLocked(room, participantID, sessionToken)
	if authErr != nil {
		room.mu.Unlock()
		return nil, nil, nil, authErr
	}

	participant.Connected = true
	participant.UpdatedAt = nowUTC()
	subscriberID := randomHex(12)
	ch := make(chan []byte, 16)
	room.subscribers[subscriberID] = &subscriber{participantID: participantID, ch: ch}
	room.UpdatedAt = nowUTC()

	if err := m.store.Save(room); err != nil {
		room.mu.Unlock()
		close(ch)
		delete(room.subscribers, subscriberID)
		return nil, nil, nil, err
	}
	room.mu.Unlock()

	initial, marshalErr := m.marshalRoomState(room, participantID)
	if marshalErr != nil {
		room.mu.Lock()
		delete(room.subscribers, subscriberID)
		close(ch)
		room.mu.Unlock()
		return nil, nil, nil, marshalErr
	}

	unsubscribe := func() {
		roomRef, getErr := m.getRoom(roomID)
		if getErr != nil {
			return
		}

		roomRef.mu.Lock()
		_, exists := roomRef.subscribers[subscriberID]
		if !exists {
			roomRef.mu.Unlock()
			return
		}
		delete(roomRef.subscribers, subscriberID)

		if p, participantOK := roomRef.Participants[participantID]; participantOK {
			if p.Connected {
				m.disconnectParticipantLocked(roomRef, p)
				m.appendActivityLogLocked(roomRef, "%s disconnected.", p.Username)
				_ = m.store.Save(roomRef)
			}
		}
		roomRef.mu.Unlock()

		go m.broadcastRoom(roomID)
	}

	go m.broadcastRoom(roomID)
	return ch, initial, unsubscribe, nil
}

func (m *Manager) getRoom(roomID string) (*Room, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	room, ok := m.rooms[roomID]
	if !ok {
		return nil, ErrRoomNotFound
	}
	return room, nil
}

func (m *Manager) authorizeParticipantLocked(room *Room, participantID, sessionToken string) (*Participant, error) {
	participant, ok := room.Participants[participantID]
	if !ok {
		return nil, ErrParticipantNotFound
	}
	if !secureTokenMatches(participant.SessionToken, sessionToken) {
		return nil, ErrUnauthorized
	}
	return participant, nil
}

func (m *Manager) loadFromDisk() error {
	persistedRooms, err := m.store.LoadAll()
	if err != nil {
		return err
	}

	for _, persisted := range persistedRooms {
		allowVoteChange := true
		if persisted.Settings.AllowVoteChange != nil {
			allowVoteChange = *persisted.Settings.AllowVoteChange
		}
		settings := RoomSettings{
			RoomName:         persisted.Settings.RoomName,
			MaxPeople:        persisted.Settings.MaxPeople,
			Cards:            append([]string(nil), persisted.Settings.Cards...),
			AllowSpectators:  persisted.Settings.AllowSpectators,
			AnonymousVoting:  persisted.Settings.AnonymousVoting,
			AutoReset:        persisted.Settings.AutoReset,
			AllowVoteChange:  allowVoteChange,
			RevealMode:       persisted.Settings.RevealMode,
			VotingTimeoutSec: persisted.Settings.VotingTimeoutSec,
			PasswordSalt:     persisted.Settings.PasswordSalt,
			PasswordHash:     persisted.Settings.PasswordHash,
		}

		room := &Room{
			ID:           persisted.ID,
			AdminToken:   persisted.AdminToken,
			CreatedAt:    persisted.CreatedAt,
			UpdatedAt:    persisted.UpdatedAt,
			Settings:     settings,
			Round:        persisted.Round,
			Participants: make(map[string]*Participant, len(persisted.Participants)),
			ActivityLog:  append([]ActivityLogEntry(nil), persisted.ActivityLog...),
			subscribers:  map[string]*subscriber{},
		}
		for _, participant := range persisted.Participants {
			sessionToken := participant.SessionToken
			if sessionToken == "" {
				sessionToken = randomHex(24)
			}
			room.Participants[participant.ID] = &Participant{
				ID:           participant.ID,
				SessionToken: sessionToken,
				Username:     participant.Username,
				Role:         participant.Role,
				IsAdmin:      participant.IsAdmin,
				Connected:    false,
				HasVoted:     participant.HasVoted,
				VoteValue:    participant.VoteValue,
				JoinedAt:     participant.JoinedAt,
				UpdatedAt:    participant.UpdatedAt,
			}
		}

		m.rooms[room.ID] = room
	}

	return nil
}

func (room *Room) toPersisted() persistedRoom {
	allowVoteChange := room.Settings.AllowVoteChange
	participants := make([]*persistedParticipant, 0, len(room.Participants))
	for _, participant := range sortParticipants(room.Participants) {
		participants = append(participants, &persistedParticipant{
			ID:           participant.ID,
			SessionToken: participant.SessionToken,
			Username:     participant.Username,
			Role:         participant.Role,
			IsAdmin:      participant.IsAdmin,
			Connected:    participant.Connected,
			HasVoted:     participant.HasVoted,
			VoteValue:    participant.VoteValue,
			JoinedAt:     participant.JoinedAt,
			UpdatedAt:    participant.UpdatedAt,
		})
	}

	return persistedRoom{
		ID:           room.ID,
		AdminToken:   room.AdminToken,
		CreatedAt:    room.CreatedAt,
		UpdatedAt:    room.UpdatedAt,
		Settings: persistedRoomSettings{
			RoomName:         room.Settings.RoomName,
			MaxPeople:        room.Settings.MaxPeople,
			Cards:            append([]string(nil), room.Settings.Cards...),
			AllowSpectators:  room.Settings.AllowSpectators,
			AnonymousVoting:  room.Settings.AnonymousVoting,
			AutoReset:        room.Settings.AutoReset,
			AllowVoteChange:  &allowVoteChange,
			RevealMode:       room.Settings.RevealMode,
			VotingTimeoutSec: room.Settings.VotingTimeoutSec,
			PasswordSalt:     room.Settings.PasswordSalt,
			PasswordHash:     room.Settings.PasswordHash,
		},
		Round:        room.Round,
		Participants: participants,
		ActivityLog:  append([]ActivityLogEntry(nil), room.ActivityLog...),
	}
}

func allActiveParticipantsVoted(room *Room) bool {
	activeParticipants := 0
	for _, participant := range room.Participants {
		if participant.Role != RoleParticipant || !participant.Connected {
			continue
		}
		activeParticipants++
		if !participant.HasVoted {
			return false
		}
	}
	return activeParticipants > 0
}

func (m *Manager) resetVotesLocked(room *Room) {
	room.Round.Revealed = false
	for _, participant := range room.Participants {
		if participant.Role != RoleParticipant {
			continue
		}
		participant.HasVoted = false
		participant.VoteValue = ""
		participant.UpdatedAt = nowUTC()
	}
}

func (m *Manager) marshalRoomState(room *Room, viewerParticipantID string) ([]byte, error) {
	room.mu.RLock()
	defer room.mu.RUnlock()

	viewer, ok := room.Participants[viewerParticipantID]
	if !ok {
		return nil, ErrParticipantNotFound
	}

	participants := make([]PublicParticipant, 0, len(room.Participants))
	for _, participant := range sortParticipants(room.Participants) {
		if !participant.Connected {
			continue
		}

		public := PublicParticipant{
			ID:        participant.ID,
			Username:  participant.Username,
			Role:      participant.Role,
			IsAdmin:   participant.IsAdmin,
			Connected: participant.Connected,
			HasVoted:  participant.HasVoted,
		}

		if room.Round.Revealed {
			public.VoteValue = participant.VoteValue
		} else if participant.ID == viewerParticipantID {
			public.VoteValue = participant.VoteValue
		}

		participants = append(participants, public)
	}

	state := PublicRoomState{
		RoomID:            room.ID,
		RoomName:          room.Settings.RoomName,
		Cards:             append([]string(nil), room.Settings.Cards...),
		Revealed:          room.Round.Revealed,
		RevealMode:        room.Settings.RevealMode,
		MaxPeople:         room.Settings.MaxPeople,
		AllowSpectators:   room.Settings.AllowSpectators,
		AnonymousVoting:   room.Settings.AnonymousVoting,
		AutoReset:         room.Settings.AutoReset,
		AllowVoteChange:   room.Settings.AllowVoteChange,
		VotingTimeoutSec:  room.Settings.VotingTimeoutSec,
		Participants:      participants,
		SelfParticipantID: viewerParticipantID,
		ViewerIsAdmin:     viewer.IsAdmin,
		Links: RoomLinks{
			ParticipantLink: "/room/" + room.ID,
		},
	}

	if viewer.IsAdmin {
		state.Links.AdminLink = "/room/" + room.ID + "?adminToken=" + room.AdminToken

		start := 0
		if len(room.ActivityLog) > adminLogBroadcastLimit {
			start = len(room.ActivityLog) - adminLogBroadcastLimit
		}
		state.AdminLogs = make([]PublicActivityLogEntry, 0, len(room.ActivityLog)-start)
		for _, item := range room.ActivityLog[start:] {
			state.AdminLogs = append(state.AdminLogs, PublicActivityLogEntry{
				At:      item.At,
				Message: item.Message,
			})
		}
	}

	return json.Marshal(state)
}

func (m *Manager) broadcastRoom(roomID string) {
	room, err := m.getRoom(roomID)
	if err != nil {
		return
	}

	type target struct {
		participantID string
		ch            chan []byte
	}

	room.mu.RLock()
	targets := make([]target, 0, len(room.subscribers))
	for _, subscriber := range room.subscribers {
		targets = append(targets, target{participantID: subscriber.participantID, ch: subscriber.ch})
	}
	room.mu.RUnlock()

	for _, t := range targets {
		payload, marshalErr := m.marshalRoomState(room, t.participantID)
		if marshalErr != nil {
			continue
		}
		select {
		case t.ch <- payload:
		default:
		}
	}
}

func (m *Manager) appendActivityLogLocked(room *Room, format string, args ...any) {
	room.ActivityLog = append(room.ActivityLog, ActivityLogEntry{
		At:      nowUTC(),
		Message: fmt.Sprintf(format, args...),
	})

	if len(room.ActivityLog) > maxActivityLogEntries {
		room.ActivityLog = append([]ActivityLogEntry(nil), room.ActivityLog[len(room.ActivityLog)-maxActivityLogEntries:]...)
	}
}

func (m *Manager) disconnectParticipantLocked(room *Room, participant *Participant) {
	participant.Connected = false
	participant.UpdatedAt = nowUTC()
	room.UpdatedAt = nowUTC()
}
