// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

package irc

import (
	"context"
	"strings"
	"sync"
	"time"

	"github.com/autobrr/autobrr/internal/domain"
	"github.com/autobrr/autobrr/internal/indexer"
	"github.com/autobrr/autobrr/internal/logger"
	"github.com/autobrr/autobrr/internal/notification"
	"github.com/autobrr/autobrr/internal/release"
	"github.com/autobrr/autobrr/pkg/errors"

	"github.com/rs/zerolog"
)

type Service interface {
	StartHandlers()
	StopHandlers()
	StopNetwork(key handlerKey) error
	RestartNetwork(ctx context.Context, id int64) error
	ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
	GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error)
	GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error)
	DeleteNetwork(ctx context.Context, id int64) error
	StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error
	UpdateNetwork(ctx context.Context, network *domain.IrcNetwork) error
	StoreChannel(networkID int64, channel *domain.IrcChannel) error
}

type service struct {
	stopWG sync.WaitGroup
	lock   sync.RWMutex

	log                 zerolog.Logger
	repo                domain.IrcRepo
	releaseService      release.Service
	indexerService      indexer.Service
	notificationService notification.Service
	indexerMap          map[string]string
	handlers            map[handlerKey]*Handler
}

func NewService(log logger.Logger, repo domain.IrcRepo, releaseSvc release.Service, indexerSvc indexer.Service, notificationSvc notification.Service) Service {
	return &service{
		log:                 log.With().Str("module", "irc").Logger(),
		repo:                repo,
		releaseService:      releaseSvc,
		indexerService:      indexerSvc,
		notificationService: notificationSvc,
		handlers:            make(map[handlerKey]*Handler),
	}
}

type handlerKey struct {
	server string
	nick   string
}

func (s *service) StartHandlers() {
	networks, err := s.repo.FindActiveNetworks(context.Background())
	if err != nil {
		s.log.Error().Err(err).Msg("failed to list networks")
	}

	for _, network := range networks {
		if !network.Enabled {
			continue
		}

		// check if already in handlers
		//v, ok := s.handlers[network.Name]

		s.lock.Lock()
		channels, err := s.repo.ListChannels(network.ID)
		if err != nil {
			s.log.Error().Err(err).Msgf("failed to list channels for network %q", network.Server)
		}
		network.Channels = channels

		// find indexer definitions for network and add
		definitions := s.indexerService.GetIndexersByIRCNetwork(network.Server)

		// init new irc handler
		handler := NewHandler(s.log, network, definitions, s.releaseService, s.notificationService)

		// use network.Server + nick to use multiple indexers with different nick per network
		// this allows for multiple handlers to one network
		s.handlers[handlerKey{network.Server, network.Nick}] = handler
		s.lock.Unlock()

		s.log.Debug().Msgf("starting network: %+v", network.Name)

		go func(network domain.IrcNetwork) {
			if err := handler.Run(); err != nil {
				s.log.Error().Err(err).Msgf("failed to start handler for network %q", network.Name)
			}
		}(network)
	}
}

func (s *service) StopHandlers() {
	for _, handler := range s.handlers {
		s.log.Info().Msgf("stopping network: %+v", handler.network.Name)
		handler.Stop()
	}

	s.log.Info().Msg("stopped all irc handlers")
}

func (s *service) startNetwork(network domain.IrcNetwork) error {
	// look if we have the network in handlers already, if so start it
	if existingHandler, found := s.handlers[handlerKey{network.Server, network.Nick}]; found {
		s.log.Debug().Msgf("starting network: %+v", network.Name)

		if !existingHandler.client.Connected() {
			go func(handler *Handler) {
				if err := handler.Run(); err != nil {
					s.log.Error().Err(err).Msgf("failed to start existingHandler for network %q", handler.network.Name)
				}
			}(existingHandler)
		}
	} else {
		// if not found in handlers, lets add it and run it

		s.lock.Lock()
		channels, err := s.repo.ListChannels(network.ID)
		if err != nil {
			s.log.Error().Err(err).Msgf("failed to list channels for network %q", network.Server)
		}
		network.Channels = channels

		// find indexer definitions for network and add
		definitions := s.indexerService.GetIndexersByIRCNetwork(network.Server)

		// init new irc handler
		handler := NewHandler(s.log, network, definitions, s.releaseService, s.notificationService)

		s.handlers[handlerKey{network.Server, network.Nick}] = handler
		s.lock.Unlock()

		s.log.Debug().Msgf("starting network: %+v", network.Name)

		go func(network domain.IrcNetwork) {
			if err := handler.Run(); err != nil {
				s.log.Error().Err(err).Msgf("failed to start handler for network %q", network.Name)
			}
		}(network)
	}

	return nil
}

func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error {
	// look if we have the network in handlers, if so restart it
	if existingHandler, found := s.handlers[handlerKey{network.Server, network.Nick}]; found {
		s.log.Debug().Msgf("irc: decide if irc network handler needs restart or updating: %+v", network.Server)

		// if server, tls, invite command, port : changed - restart
		// if nickserv account, nickserv password : changed - stay connected, and change those
		// if channels len : changes - join or leave
		if existingHandler.client.Connected() {
			handler := existingHandler.GetNetwork()
			restartNeeded := false

			if handler.Server != network.Server {
				restartNeeded = true
			} else if handler.Port != network.Port {
				restartNeeded = true
			} else if handler.TLS != network.TLS {
				restartNeeded = true
			} else if handler.InviteCommand != network.InviteCommand {
				restartNeeded = true
			}
			if restartNeeded {
				s.log.Info().Msgf("irc: restarting network: %+v", network.Server)

				// we need to reinitialize with new network config
				existingHandler.UpdateNetwork(network)

				// todo reset channelHealth?

				go func() {
					if err := existingHandler.Restart(); err != nil {
						s.log.Error().Stack().Err(err).Msgf("failed to restart network %q", existingHandler.network.Name)
					}
				}()

				// return now since the restart will read the network again
				return nil
			}

			if handler.Nick != network.Nick {
				s.log.Debug().Msg("changing nick")

				if err := existingHandler.NickChange(network.Nick); err != nil {
					s.log.Error().Stack().Err(err).Msgf("failed to change nick %q", network.Nick)
				}
			} else if handler.Auth.Password != network.Auth.Password {
				s.log.Debug().Msg("nickserv: changing password")

				if err := existingHandler.NickServIdentify(network.Auth.Password); err != nil {
					s.log.Error().Stack().Err(err).Msgf("failed to identify with nickserv %q", network.Nick)
				}
			}

			// join or leave channels
			// loop over handler channels,
			var expectedChannels = make(map[string]struct{}, 0)
			var handlerChannels = make(map[string]struct{}, 0)
			var channelsToLeave = make([]string, 0)
			var channelsToJoin = make([]domain.IrcChannel, 0)

			// create map of expected channels
			for _, channel := range network.Channels {
				expectedChannels[channel.Name] = struct{}{}
			}

			// check current channels of handler against expected
			for _, handlerChan := range handler.Channels {
				handlerChannels[handlerChan.Name] = struct{}{}

				_, ok := expectedChannels[handlerChan.Name]
				if ok {
					// 	if handler channel matches network channel next
					continue
				}

				// if not expected, leave
				channelsToLeave = append(channelsToLeave, handlerChan.Name)
			}

			// check new channels against handler to see which to join
			for _, channel := range network.Channels {
				_, ok := handlerChannels[channel.Name]
				if ok {
					continue
				}

				// if expected channel not in handler channels, add to join
				// use channel struct for extra info
				channelsToJoin = append(channelsToJoin, channel)
			}

			// leave channels
			for _, leaveChannel := range channelsToLeave {
				s.log.Debug().Msgf("%v: part channel %v", network.Server, leaveChannel)

				if err := existingHandler.PartChannel(leaveChannel); err != nil {
					s.log.Error().Stack().Err(err).Msgf("failed to leave channel: %q", leaveChannel)
				}
			}

			// join channels
			for _, joinChannel := range channelsToJoin {
				s.log.Debug().Msgf("%v: join new channel %v", network.Server, joinChannel)

				if err := existingHandler.JoinChannel(joinChannel.Name, joinChannel.Password); err != nil {
					s.log.Error().Stack().Err(err).Msgf("failed to join channel: %q", joinChannel.Name)
				}
			}

			// update network for handler
			// TODO move all this restart logic inside handler to let it decide what to do
			existingHandler.SetNetwork(network)

			// find indexer definitions for network and add
			definitions := s.indexerService.GetIndexersByIRCNetwork(network.Server)

			existingHandler.InitIndexers(definitions)
		}
	} else {
		if err := s.startNetwork(*network); err != nil {
			s.log.Error().Stack().Err(err).Msgf("failed to start network: %q", network.Name)
		}
	}

	return nil
}

func (s *service) RestartNetwork(ctx context.Context, id int64) error {
	network, err := s.repo.GetNetworkByID(ctx, id)
	if err != nil {
		return err
	}

	return s.restartNetwork(*network)
}

func (s *service) restartNetwork(network domain.IrcNetwork) error {
	// look if we have the network in handlers, if so restart it
	if existingHandler, found := s.handlers[handlerKey{network.Server, network.Nick}]; found {
		s.log.Info().Msgf("restarting network: %v", network.Name)

		if existingHandler.client.Connected() {
			go func() {
				if err := existingHandler.Restart(); err != nil {
					s.log.Error().Err(err).Msgf("failed to restart network %q", existingHandler.network.Name)
				}
			}()
		}
	}

	// TODO handle full restart

	return nil
}

func (s *service) StopNetwork(key handlerKey) error {
	if handler, found := s.handlers[key]; found {
		handler.Stop()
		s.log.Debug().Msgf("stopped network: %+v", key.server)
	}

	return nil
}

func (s *service) StopAndRemoveNetwork(key handlerKey) error {
	if handler, found := s.handlers[key]; found {
		handler.Stop()

		// remove from handlers
		delete(s.handlers, key)
		s.log.Debug().Msgf("stopped network: %+v", key)
	}

	return nil
}

func (s *service) StopNetworkIfRunning(key handlerKey) error {
	if handler, found := s.handlers[key]; found {
		handler.Stop()
		s.log.Debug().Msgf("stopped network: %+v", key.server)
	}

	return nil
}

func (s *service) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error) {
	network, err := s.repo.GetNetworkByID(ctx, id)
	if err != nil {
		s.log.Error().Err(err).Msgf("failed to get network: %v", id)
		return nil, err
	}

	channels, err := s.repo.ListChannels(network.ID)
	if err != nil {
		s.log.Error().Err(err).Msgf("failed to list channels for network %q", network.Server)
		return nil, err
	}
	network.Channels = append(network.Channels, channels...)

	return network, nil
}

func (s *service) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
	networks, err := s.repo.ListNetworks(ctx)
	if err != nil {
		s.log.Error().Err(err).Msg("failed to list networks")
		return nil, err
	}

	var ret []domain.IrcNetwork

	for _, n := range networks {
		channels, err := s.repo.ListChannels(n.ID)
		if err != nil {
			s.log.Error().Msgf("failed to list channels for network %q: %v", n.Server, err)
			return nil, err
		}
		n.Channels = append(n.Channels, channels...)

		ret = append(ret, n)
	}

	return ret, nil
}

func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error) {
	networks, err := s.repo.ListNetworks(ctx)
	if err != nil {
		s.log.Error().Err(err).Msg("failed to list networks")
		return nil, err
	}

	var ret []domain.IrcNetworkWithHealth

	for _, n := range networks {
		netw := domain.IrcNetworkWithHealth{
			ID:               n.ID,
			Name:             n.Name,
			Enabled:          n.Enabled,
			Server:           n.Server,
			Port:             n.Port,
			TLS:              n.TLS,
			Pass:             n.Pass,
			Nick:             n.Nick,
			Auth:             n.Auth,
			InviteCommand:    n.InviteCommand,
			Connected:        false,
			Channels:         []domain.ChannelWithHealth{},
			ConnectionErrors: []string{},
		}

		handler, ok := s.handlers[handlerKey{n.Server, n.Nick}]
		if ok {
			handler.m.RLock()

			// only set connected and connected since if we have an active handler and connection
			if handler.client.Connected() {

				netw.Connected = handler.connectedSince != time.Time{}
				netw.ConnectedSince = handler.connectedSince

				// current and preferred nick is only available if the network is connected
				netw.CurrentNick = handler.CurrentNick()
				netw.PreferredNick = handler.PreferredNick()
			}
			netw.Healthy = handler.Healthy()

			// if we have any connection errors like bad nickserv auth add them here
			if len(handler.connectionErrors) > 0 {
				netw.ConnectionErrors = handler.connectionErrors
			}

			handler.m.RUnlock()
		}

		channels, err := s.repo.ListChannels(n.ID)
		if err != nil {
			s.log.Error().Msgf("failed to list channels for network %q: %v", n.Server, err)
			return nil, err
		}

		// combine from repo and handler
		for _, channel := range channels {
			ch := domain.ChannelWithHealth{
				ID:       channel.ID,
				Enabled:  channel.Enabled,
				Name:     channel.Name,
				Password: channel.Password,
				Detached: channel.Detached,
				//Monitoring:      false,
				//MonitoringSince: time.Time{},
				//LastAnnounce:    time.Time{},
			}

			// only check if we have a handler
			if handler != nil {
				name := strings.ToLower(channel.Name)

				handler.m.RLock()
				chan1, ok := handler.channelHealth[name]
				if ok {
					chan1.m.RLock()
					ch.Monitoring = chan1.monitoring
					ch.MonitoringSince = chan1.monitoringSince
					ch.LastAnnounce = chan1.lastAnnounce

					chan1.m.RUnlock()
				}
				handler.m.RUnlock()
			}

			netw.Channels = append(netw.Channels, ch)
		}

		ret = append(ret, netw)

	}

	return ret, nil
}

func (s *service) DeleteNetwork(ctx context.Context, id int64) error {
	network, err := s.GetNetworkByID(ctx, id)
	if err != nil {
		s.log.Error().Stack().Err(err).Msgf("could not find network before delete: %v", network.Name)
		return err
	}

	s.log.Debug().Msgf("delete network: %v", id)

	// Remove network and handler
	//if err = s.StopNetwork(network.Server); err != nil {
	if err = s.StopAndRemoveNetwork(handlerKey{network.Server, network.Nick}); err != nil {
		s.log.Error().Stack().Err(err).Msgf("could not stop and delete network: %v", network.Name)
		return err
	}

	if err = s.repo.DeleteNetwork(ctx, id); err != nil {
		s.log.Error().Stack().Err(err).Msgf("could not delete network: %v", network.Name)
		return err
	}

	return nil
}

func (s *service) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork) error {

	if network.Channels != nil {
		if err := s.repo.StoreNetworkChannels(ctx, network.ID, network.Channels); err != nil {
			return err
		}
	}

	if err := s.repo.UpdateNetwork(ctx, network); err != nil {
		return err
	}
	s.log.Debug().Msgf("irc.service: update network: %+v", network)

	// stop or start network
	// TODO get current state to see if enabled or not?
	if network.Enabled {
		// if server, tls, invite command, port : changed - restart
		// if nickserv account, nickserv password : changed - stay connected, and change those
		// if channels len : changes - join or leave
		err := s.checkIfNetworkRestartNeeded(network)
		if err != nil {
			s.log.Error().Stack().Err(err).Msgf("could not restart network: %+v", network.Name)
			return errors.New("could not restart network: %v", network.Name)
		}

	} else {
		// take into account multiple channels per network
		err := s.StopAndRemoveNetwork(handlerKey{network.Server, network.Nick})
		if err != nil {
			s.log.Error().Stack().Err(err).Msgf("could not stop network: %+v", network.Name)
			return errors.New("could not stop network: %v", network.Name)
		}
	}

	return nil
}

func (s *service) StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error {
	existingNetwork, err := s.repo.CheckExistingNetwork(ctx, network)
	if err != nil {
		s.log.Error().Err(err).Msg("could not check for existing network")
		return err
	}

	if existingNetwork == nil {
		if err := s.repo.StoreNetwork(network); err != nil {
			return err
		}
		s.log.Debug().Msgf("store network: %+v", network)

		if network.Channels != nil {
			for _, channel := range network.Channels {
				if err := s.repo.StoreChannel(network.ID, &channel); err != nil {
					s.log.Error().Stack().Err(err).Msg("irc.storeChannel: error executing query")
					return errors.Wrap(err, "error storing channel on network")
					//return err
				}
			}
		}

		return nil
	}

	// get channels for existing network
	existingChannels, err := s.repo.ListChannels(existingNetwork.ID)
	if err != nil {
		s.log.Error().Err(err).Msgf("failed to list channels for network %q", existingNetwork.Server)
	}
	existingNetwork.Channels = existingChannels

	if network.Channels != nil {
		for _, channel := range network.Channels {
			// add channels. Make sure it doesn't delete before
			if err := s.repo.StoreChannel(existingNetwork.ID, &channel); err != nil {
				return err
			}
		}

		// append channels to existing network
		existingNetwork.Channels = append(existingNetwork.Channels, network.Channels...)
	}

	// append invite command for existing network
	if network.InviteCommand != "" {
		existingNetwork.InviteCommand = strings.Join([]string{existingNetwork.InviteCommand, network.InviteCommand}, ",")
		if err := s.repo.UpdateInviteCommand(existingNetwork.ID, existingNetwork.InviteCommand); err != nil {
			return err
		}
	}

	if existingNetwork.Enabled {
		// if server, tls, invite command, port : changed - restart
		// if nickserv account, nickserv password : changed - stay connected, and change those
		// if channels len : changes - join or leave

		err := s.checkIfNetworkRestartNeeded(existingNetwork)
		if err != nil {
			s.log.Error().Err(err).Msgf("could not restart network: %+v", existingNetwork.Name)
			return errors.New("could not restart network: %v", existingNetwork.Name)
		}
	}

	return nil
}

func (s *service) StoreChannel(networkID int64, channel *domain.IrcChannel) error {
	if err := s.repo.StoreChannel(networkID, channel); err != nil {
		return err
	}

	return nil
}
