package bot

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"

	"codeberg.org/nextgo/dbots/internal/db"
	"codeberg.org/nextgo/dbots/internal/discord"
	"codeberg.org/nextgo/dbots/internal/errorutil"
	"codeberg.org/nextgo/dbots/internal/middleware"
	"codeberg.org/nextgo/dbots/internal/paginate"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
)

type DiscordApiBot struct {
	Application DiscordApplication `json:"application"`
	Bot         DiscordBot         `json:"bot"`
}

type DiscordApplication struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Icon        string `json:"icon"`
	Description string `json:"description"`
	IsVerified  bool   `json:"is_verified"`
	BotPublic   bool   `json:"bot_public"`
}

type DiscordBot struct {
	ID                    string `json:"id"`
	Username              string `json:"username"`
	Avatar                string `json:"avatar"`
	Bot                   bool   `json:"bot"`
	ApproximateGuildCount int32  `json:"approximate_guild_count"`
}

type Service struct {
	q      *db.Queries
	client *discord.Client
}

func NewService(q *db.Queries, client *discord.Client) *Service {
	return &Service{q: q, client: client}
}

func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, error) {
	user := middleware.GetUser(ctx)
	application, err := s.client.GetApplication(ctx, data.ID)
	if err != nil {
		return nil, errorutil.ErrBotNotExists // todo: some old bots have different client id (not the same as the user id)
	}

	var count int32
	b, err := s.q.CreateBot(ctx, db.CreateBotParams{
		ID:             data.ID,
		Overview:       &data.Overview,
		IsSlash:        data.IsSlash,
		InstallContext: data.InstallContext,
		ImportedFrom:   nil, // reserved for custom endpoint, should always be nil here ok??
		Username:       application.Name,
		Avatar:         &application.Icon,
		Description:    &data.Description,
		GuildCount:     &count,
		InstallCount:   &count,
		Prefix:         data.Prefix,
		MainOwnerID:    &user.ID,
	})
	if err != nil {
		slog.Error("error submitting bot", "err", err, "bot_id", data.ID)
		var pgErr *pgconn.PgError
		if errors.As(err, &pgErr) && pgErr.Code == "23505" {
			return nil, errorutil.ErrBotAlreadyExists
		}
		return nil, err
	}

	if err := s.addCoOwners(ctx, b.ID, user.ID, data.CoOwners); err != nil {
		// Bot was created but co-owners failed. Log and surface the error.
		// ideally this whole operation runs in a transaction but who cares lol.
		slog.Error("error adding co-owners after submit", "err", err, "bot_id", b.ID)
		// so if co-owners fail it's because the co-owner isn't registered in the botlist,
		// however, i should handle this but i'm lazy as fuck. for now i will only output
		// a silent error log instead of aborting the whole request.
	}

	return b, nil
}

// AddCoOwner adds a single co-owner to a bot.
// Only the main owner of the bot may call this.
func (s *Service) AddCoOwner(ctx context.Context, botID, coOwnerID string) error {
	user := middleware.GetUser(ctx)

	bot, err := s.q.GetBot(ctx, botID)
	if err != nil {
		return errorutil.ErrNotFound.Err
	}

	if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
		return errorutil.ErrForbidden.Err
	}

	if err := validateCoOwner(coOwnerID, user.ID); err != nil {
		return err
	}

	if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
		BotID:  botID,
		UserID: coOwnerID,
	}); err != nil {
		slog.Error("error adding co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
		return err
	}

	return nil
}

// RemoveCoOwner removes a co-owner from a bot.
// Only the main owner of the bot may call this.
func (s *Service) RemoveCoOwner(ctx context.Context, botID, coOwnerID string) error {
	user := middleware.GetUser(ctx)

	bot, err := s.q.GetBot(ctx, botID)
	if err != nil {
		return errorutil.ErrNotFound.Err
	}

	if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
		return errorutil.ErrForbidden.Err
	}

	if err := s.q.RemoveBotCoOwner(ctx, db.RemoveBotCoOwnerParams{
		BotID:  botID,
		UserID: coOwnerID,
	}); err != nil {
		slog.Error("error removing co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
		return err
	}

	return nil
}

// ListCoOwners returns the users who are co-owners of a bot.
func (s *Service) ListCoOwners(ctx context.Context, botID string) ([]*db.User, error) {
	owners, err := s.q.ListCoOwnersByBot(ctx, botID)
	if err != nil {
		slog.Error("error listing co-owners", "err", err, "bot_id", botID)
		return nil, err
	}
	return owners, nil
}

func (s *Service) Get(ctx context.Context, id string) (*db.Bot, error) {
	bot, err := s.q.GetBot(ctx, id)
	if err != nil {
		slog.Error("error getting bot", "err", err, "id", id)
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errorutil.ErrNotFound.Err
		} else {
			return nil, err
		}
	}

	return bot, nil
}

func (s *Service) List(
	ctx context.Context,
	query string,
	p paginate.Params,
) (paginate.Page[*db.Bot], error) {
	total, err := s.q.CountBotsByUsername(ctx, db.CountBotsByUsernameParams{
		Status: db.BotStatusApproved,
		Query:  query,
	})
	if err != nil {
		slog.Error("error counting bots", "query", query, "err", err)
		return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
	}

	bots, err := s.q.ListBotsByStatus(ctx, db.ListBotsByStatusParams{
		Status: db.BotStatusApproved,
		Limit:  p.Limit,
		Offset: p.Offset,
	})
	if err != nil {
		slog.Error("error listing bots", "query", query, "err", err)
		return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
	}

	return paginate.NewPage(bots, int(total), p), nil
}

// validateCoOwner checks that the given ID is a valid Discord snowflake
// and is not the same as the main owner.
func validateCoOwner(id, mainOwnerID string) error {
	if _, err := strconv.ParseUint(id, 10, 64); err != nil {
		return errorutil.ErrInvalidID
	}
	if id == mainOwnerID {
		return errorutil.ErrMainOwnerAsCoOwner
	}
	return nil
}

// addCoOwners validates and inserts each co-owner for a bot.
// mainOwnerID is provided to reject attempts to set the main owner as a co-owner.
func (s *Service) addCoOwners(ctx context.Context, botID, mainOwnerID string, coOwnerIDs []string) error {
	for _, id := range coOwnerIDs {
		if err := validateCoOwner(id, mainOwnerID); err != nil {
			return err
		}
		if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
			BotID:  botID,
			UserID: id,
		}); err != nil {
			return fmt.Errorf("adding co-owner %s: %w", id, err)
		}
	}
	return nil
}
