package psql

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gabehf/koito/internal/db"
	"github.com/gabehf/koito/internal/logger"
	"github.com/gabehf/koito/internal/models"
	"github.com/gabehf/koito/internal/repository"
	"github.com/gabehf/koito/internal/utils"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Album, error) {
	l := logger.FromContext(ctx)
	var err error
	var ret = new(models.Album)

	if opts.MusicBrainzID != uuid.Nil {
		l.Debug().Msgf("Fetching album from DB with MusicBrainz Release ID %s", opts.MusicBrainzID)
		row, err := d.q.GetReleaseByMbzID(ctx, &opts.MusicBrainzID)
		if err != nil {
			return nil, fmt.Errorf("GetAlbum: %w", err)
		}
		opts.ID = row.ID
	} else if opts.ArtistID != 0 && opts.Title != "" {
		l.Debug().Msgf("Fetching album from DB with artist_id %d and title %s", opts.ArtistID, opts.Title)
		row, err := d.q.GetReleaseByArtistAndTitle(ctx, repository.GetReleaseByArtistAndTitleParams{
			ArtistID: opts.ArtistID,
			Title:    opts.Title,
		})
		if err != nil {
			return nil, fmt.Errorf("GetAlbum: %w", err)
		}
		opts.ID = row.ID
	} else if opts.ArtistID != 0 && len(opts.Titles) > 0 {
		l.Debug().Msgf("Fetching release group from DB with artist_id %d and titles %v", opts.ArtistID, opts.Titles)
		row, err := d.q.GetReleaseByArtistAndTitles(ctx, repository.GetReleaseByArtistAndTitlesParams{
			ArtistID: opts.ArtistID,
			Column1:  opts.Titles,
		})
		if err != nil {
			return nil, fmt.Errorf("GetAlbum: %w", err)
		}
		opts.ID = row.ID
	}

	l.Debug().Msgf("Fetching album from DB with id %d", opts.ID)
	row, err := d.q.GetRelease(ctx, opts.ID)
	if err != nil {
		return nil, fmt.Errorf("GetAlbum: %w", err)
	}

	count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
		ListenedAt:   time.Unix(0, 0),
		ListenedAt_2: time.Now(),
		ReleaseID:    opts.ID,
	})
	if err != nil {
		return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err)
	}

	seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
		Timeframe: db.Timeframe{Period: db.PeriodAllTime},
		AlbumID:   opts.ID,
	})
	if err != nil {
		return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
	}

	firstListen, err := d.q.GetFirstListenFromRelease(ctx, opts.ID)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err)
	}

	rank, err := d.q.GetReleaseAllTimeRank(ctx, opts.ID)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, fmt.Errorf("GetAlbum: GetReleaseAllTimeRank: %w", err)
	}

	ret.ID = row.ID
	ret.MbzID = row.MusicBrainzID
	ret.Title = row.Title
	ret.Image = row.Image
	ret.VariousArtists = row.VariousArtists
	err = json.Unmarshal(row.Artists, &ret.Artists)
	if err != nil {
		return nil, fmt.Errorf("GetAlbum: json.Unmarshal: %w", err)
	}
	ret.AllTimeRank = rank.Rank
	ret.ListenCount = count
	ret.TimeListened = seconds
	ret.FirstListen = firstListen.ListenedAt.Unix()

	return ret, nil
}

func (d *Psql) GetAlbumWithNoMbzIDByTitles(ctx context.Context, artistId int32, titles []string) (*models.Album, error) {
	l := logger.FromContext(ctx)
	ret := new(models.Album)

	if artistId != 0 && len(titles) > 0 {
		l.Debug().Msgf("GetAlbumWithNoMbzIDByTitles: Fetching release group from DB with artist_id %d and titles %v and no associated MusicBrainz ID", artistId, titles)
		row, err := d.q.GetReleaseByArtistAndTitlesNoMbzID(ctx, repository.GetReleaseByArtistAndTitlesNoMbzIDParams{
			ArtistID: artistId,
			Column1:  titles,
		})
		if err != nil {
			return nil, fmt.Errorf("GetAlbum: %w", err)
		}
		ret.ID = row.ID
		ret.MbzID = row.MusicBrainzID
		ret.Title = row.Title
		ret.Image = row.Image
		ret.VariousArtists = row.VariousArtists
	} else {
		return nil, errors.New("GetAlbumWithNoMbzIDByTitles: insufficient information to get album")
	}
	count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{
		ListenedAt:   time.Unix(0, 0),
		ListenedAt_2: time.Now(),
		ReleaseID:    ret.ID,
	})
	if err != nil {
		return nil, fmt.Errorf("GetAlbumWithNoMbzIDByTitles: CountListensFromRelease: %w", err)
	}

	seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{
		Timeframe: db.Timeframe{Period: db.PeriodAllTime},
		AlbumID:   ret.ID,
	})
	if err != nil {
		return nil, fmt.Errorf("GetAlbumWithNoMbzIDByTitles: CountTimeListenedToItem: %w", err)
	}

	firstListen, err := d.q.GetFirstListenFromRelease(ctx, ret.ID)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, fmt.Errorf("GetAlbumWithNoMbzIDByTitles: GetFirstListenFromRelease: %w", err)
	}

	ret.ListenCount = count
	ret.TimeListened = seconds
	ret.FirstListen = firstListen.ListenedAt.Unix()

	return ret, nil
}

func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Album, error) {
	l := logger.FromContext(ctx)
	var insertMbzID *uuid.UUID
	var insertImage *uuid.UUID
	if opts.MusicBrainzID != uuid.Nil {
		insertMbzID = &opts.MusicBrainzID
	}
	if opts.Image != uuid.Nil {
		insertImage = &opts.Image
	}
	if len(opts.ArtistIDs) < 1 {
		return nil, errors.New("SaveAlbum: required parameter 'ArtistIDs' missing")
	}
	for _, aid := range opts.ArtistIDs {
		if aid == 0 {
			return nil, errors.New("SaveAlbum: none of 'ArtistIDs' may be 0")
		}
	}
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return nil, fmt.Errorf("SaveAlbum: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	l.Debug().Msgf("Inserting release '%s' into DB", opts.Title)
	r, err := qtx.InsertRelease(ctx, repository.InsertReleaseParams{
		MusicBrainzID:  insertMbzID,
		VariousArtists: opts.VariousArtists,
		Image:          insertImage,
		ImageSource:    pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
	})
	if err != nil {
		return nil, fmt.Errorf("SaveAlbum: InsertRelease: %w", err)
	}
	for _, artistId := range opts.ArtistIDs {
		l.Debug().Msgf("Associating release '%s' to artist with ID %d", opts.Title, artistId)
		err = qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{
			ArtistID:  artistId,
			ReleaseID: r.ID,
			IsPrimary: opts.ArtistIDs[0] == artistId,
		})
		if err != nil {
			return nil, fmt.Errorf("SaveAlbum: AssociateArtistToRelease: %w", err)
		}
	}
	l.Debug().Msgf("Saving canonical alias %s for release %d", opts.Title, r.ID)
	err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{
		ReleaseID: r.ID,
		Alias:     opts.Title,
		Source:    "Canonical",
		IsPrimary: true,
	})
	if err != nil {
		l.Err(err).Msgf("Failed to save canonical alias for album %d", r.ID)
		return nil, fmt.Errorf("SaveAlbum: InsertReleaseAlias: %w", err)
	}

	err = tx.Commit(ctx)
	if err != nil {
		return nil, fmt.Errorf("SaveAlbum: Commit: %w", err)
	}

	err = d.SaveAlbumAliases(ctx, r.ID, opts.Aliases, "MusicBrainz")
	if err != nil {
		l.Err(err).Msgf("Failed to save aliases for album %s", opts.Title)
	}

	return &models.Album{
		ID:             r.ID,
		MbzID:          r.MusicBrainzID,
		Title:          opts.Title,
		Image:          r.Image,
		VariousArtists: r.VariousArtists,
	}, nil
}

func (d *Psql) AddArtistsToAlbum(ctx context.Context, opts db.AddArtistsToAlbumOpts) error {
	l := logger.FromContext(ctx)
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return fmt.Errorf("AddArtistsToAlbum: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	for _, id := range opts.ArtistIDs {
		err := qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{
			ReleaseID: opts.AlbumID,
			ArtistID:  id,
		})
		if err != nil {
			l.Error().Err(err).Msgf("Failed to associate release %d with artist %d", opts.AlbumID, id)
			return fmt.Errorf("AddArtistsToAlbum: AssociateArtistToRelease: %w", err)
		}
	}
	return tx.Commit(ctx)
}

func (d *Psql) UpdateAlbum(ctx context.Context, opts db.UpdateAlbumOpts) error {
	l := logger.FromContext(ctx)
	if opts.ID == 0 {
		return errors.New("missing album id")
	}
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return fmt.Errorf("UpdateAlbum: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	if opts.MusicBrainzID != uuid.Nil {
		l.Debug().Msgf("Updating release with ID %d with MusicBrainz ID %s", opts.ID, opts.MusicBrainzID)
		err := qtx.UpdateReleaseMbzID(ctx, repository.UpdateReleaseMbzIDParams{
			ID:            opts.ID,
			MusicBrainzID: &opts.MusicBrainzID,
		})
		if err != nil {
			return fmt.Errorf("UpdateAlbum: UpdateReleaseMbzID: %w", err)
		}
	}
	if opts.Image != uuid.Nil {
		if opts.ImageSrc == "" {
			return fmt.Errorf("UpdateAlbum: image source must be provided when updating an image")
		}
		l.Debug().Msgf("Updating release with ID %d with image %s", opts.ID, opts.Image)
		err := qtx.UpdateReleaseImage(ctx, repository.UpdateReleaseImageParams{
			ID:          opts.ID,
			Image:       &opts.Image,
			ImageSource: pgtype.Text{String: opts.ImageSrc, Valid: opts.ImageSrc != ""},
		})
		if err != nil {
			return fmt.Errorf("UpdateAlbum: UpdateReleaseImage: %w", err)
		}
	}
	if opts.VariousArtistsUpdate {
		l.Debug().Msgf("Updating release with ID %d with image %s", opts.ID, opts.Image)
		err := qtx.UpdateReleaseVariousArtists(ctx, repository.UpdateReleaseVariousArtistsParams{
			ID:             opts.ID,
			VariousArtists: opts.VariousArtistsValue,
		})
		if err != nil {
			return fmt.Errorf("UpdateAlbum: UpdateReleaseVariousArtists: %w", err)
		}
	}
	return tx.Commit(ctx)
}

func (d *Psql) SaveAlbumAliases(ctx context.Context, id int32, aliases []string, source string) error {
	l := logger.FromContext(ctx)
	if id == 0 {
		return errors.New("album id not specified")
	}
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return fmt.Errorf("SaveAlbumAliases: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	existing, err := qtx.GetAllReleaseAliases(ctx, id)
	if err != nil {
		return fmt.Errorf("SaveAlbumAliases: GetAllReleaseAliases: %w", err)
	}
	for _, v := range existing {
		aliases = append(aliases, v.Alias)
	}
	utils.Unique(&aliases)
	for _, alias := range aliases {
		if strings.TrimSpace(alias) == "" {
			return errors.New("SaveAlbumAliases: aliases cannot be blank")
		}
		err = qtx.InsertReleaseAlias(ctx, repository.InsertReleaseAliasParams{
			Alias:     strings.TrimSpace(alias),
			ReleaseID: id,
			Source:    source,
			IsPrimary: false,
		})
		if err != nil {
			return fmt.Errorf("SaveAlbumAliases: InsertReleaseAlias: %w", err)
		}
	}
	return tx.Commit(ctx)
}

func (d *Psql) DeleteAlbum(ctx context.Context, id int32) error {
	return d.q.DeleteRelease(ctx, id)
}
func (d *Psql) DeleteAlbumAlias(ctx context.Context, id int32, alias string) error {
	return d.q.DeleteReleaseAlias(ctx, repository.DeleteReleaseAliasParams{
		ReleaseID: id,
		Alias:     alias,
	})
}

func (d *Psql) GetAllAlbumAliases(ctx context.Context, id int32) ([]models.Alias, error) {
	rows, err := d.q.GetAllReleaseAliases(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("GetAllAlbumAliases: GetAllReleaseAliases: %w", err)
	}
	aliases := make([]models.Alias, len(rows))
	for i, row := range rows {
		aliases[i] = models.Alias{
			ID:      id,
			Alias:   row.Alias,
			Source:  row.Source,
			Primary: row.IsPrimary,
		}
	}
	return aliases, nil
}

func (d *Psql) SetPrimaryAlbumAlias(ctx context.Context, id int32, alias string) error {
	l := logger.FromContext(ctx)
	if id == 0 {
		return errors.New("artist id not specified")
	}
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return fmt.Errorf("SetPrimaryAlbumAlias: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	// get all aliases
	aliases, err := qtx.GetAllReleaseAliases(ctx, id)
	if err != nil {
		return fmt.Errorf("SetPrimaryAlbumAlias: GetAllReleaseAliases: %w", err)
	}
	primary := ""
	exists := false
	for _, v := range aliases {
		if v.Alias == alias {
			exists = true
		}
		if v.IsPrimary {
			primary = v.Alias
		}
	}
	if primary == alias {
		// no-op rename
		return nil
	}
	if !exists {
		return errors.New("SetPrimaryAlbumAlias: alias does not exist")
	}
	err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
		ReleaseID: id,
		Alias:     alias,
		IsPrimary: true,
	})
	if err != nil {
		return fmt.Errorf("SetPrimaryAlbumAlias: SetReleaseAliasPrimaryStatus: %w", err)
	}
	err = qtx.SetReleaseAliasPrimaryStatus(ctx, repository.SetReleaseAliasPrimaryStatusParams{
		ReleaseID: id,
		Alias:     primary,
		IsPrimary: false,
	})
	if err != nil {
		return fmt.Errorf("SetPrimaryAlbumAlias: SetReleaseAliasPrimaryStatus: %w", err)
	}
	return tx.Commit(ctx)
}

func (d *Psql) SetPrimaryAlbumArtist(ctx context.Context, id int32, artistId int32, value bool) error {
	l := logger.FromContext(ctx)
	if id == 0 {
		return errors.New("artist id not specified")
	}
	tx, err := d.conn.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		l.Err(err).Msg("Failed to begin transaction")
		return fmt.Errorf("SetPrimaryAlbumArtist: BeginTx: %w", err)
	}
	defer tx.Rollback(ctx)
	qtx := d.q.WithTx(tx)
	// get all artists
	artists, err := qtx.GetReleaseArtists(ctx, id)
	if err != nil {
		return fmt.Errorf("SetPrimaryAlbumArtist: GetReleaseArtists: %w", err)
	}
	var primary int32
	for _, v := range artists {
		// i dont get it??? is_primary is not a nullable column??? why use pgtype.Bool???
		// why not just use boolean??? is sqlc stupid??? am i stupid???????
		if v.IsPrimary.Valid && v.IsPrimary.Bool {
			primary = v.ID
		}
	}
	if value && primary == artistId {
		// no-op
		return nil
	}
	l.Debug().Msgf("Marking artist with id %d as 'primary = %v' on album with id %d", artistId, value, id)
	err = qtx.UpdateReleasePrimaryArtist(ctx, repository.UpdateReleasePrimaryArtistParams{
		ReleaseID: id,
		ArtistID:  artistId,
		IsPrimary: value,
	})
	if err != nil {
		return fmt.Errorf("SetPrimaryAlbumArtist: UpdateReleasePrimaryArtist: %w", err)
	}
	if value && primary != 0 {
		// if we were marking a new one as primary and there was already one marked as primary,
		// unmark that one as there can only be one
		l.Debug().Msgf("Unmarking artist with id %d as primary on album with id %d", primary, id)
		err = qtx.UpdateReleasePrimaryArtist(ctx, repository.UpdateReleasePrimaryArtistParams{
			ReleaseID: id,
			ArtistID:  primary,
			IsPrimary: false,
		})
		if err != nil {
			return fmt.Errorf("SetPrimaryAlbumArtist: UpdateReleasePrimaryArtist: %w", err)
		}
	}
	return tx.Commit(ctx)
}
