package repositories_astrologer

import (
	dto "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"
)

// Everything /astrologer/profile needs that the astrologers row does not hold.
//
// primarySkill, allSkill and languageKnown are denormalised id lists — "1",
// "1,2,26" — so the profile showed ids where the app wants names. The real
// selection lives in the astrologer_skills / astrologer_languages join tables
// the profile steps write, and the columns are the fallback: rows saved before
// those steps existed hold free text such as "Hindi", which resolves to
// nothing and is better shown as it stands than as an empty field.
//
// This mirrors resolveSkillNames / resolveLanguageNames on the user side
// (repositories/astrologer_repository.go), which serve the customer-facing
// listings from the same columns.

// GetProfileMeta collects the resolved names, the users-row pincode and the
// profile counters for one astrologer.
func (r *authRepository) GetProfileMeta(
	tx *gorm.DB,
	astrologer *models.Astrologer,
) (*dto.ProfileMeta, error) {

	if astrologer == nil {
		return &dto.ProfileMeta{}, nil
	}

	meta := &dto.ProfileMeta{}

	primary, all := r.resolveSkillNames(
		tx,
		astrologer.ID,
		astrologer.PrimarySkill,
		astrologer.AllSkill,
	)

	meta.PrimarySkillName = primary
	meta.AllSkillNames = all

	meta.LanguageNames = r.resolveLanguageNames(
		tx,
		astrologer.ID,
		astrologer.LanguageKnown,
	)

	// The address is written to the users row by step 1; astrologers has no
	// pincode or address column of its own.
	var user models.User

	if err := tx.
		Where("id = ?", astrologer.UserID).
		First(&user).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {

		return nil, err
	}

	meta.Pincode = strings.TrimSpace(user.Pincode)
	meta.Address = joinAddress(user)

	// displayName is new; a row saved before it existed carries nothing, and
	// the first word of the legal name is the sensible stand-in.
	meta.DisplayName = strings.TrimSpace(astrologer.DisplayName)

	if meta.DisplayName == "" {
		meta.DisplayName = firstWord(astrologer.Name)
	}

	meta.AverageRating, meta.TotalRating = r.ratingSummary(tx, astrologer.ID)
	meta.TotalFollowers = r.followerCount(tx, astrologer.ID)
	meta.TotalSession = r.completedSessions(tx, astrologer.ID)

	completion, err := r.profileCompletion(tx, astrologer)

	if err != nil {
		return nil, err
	}

	meta.Completion = completion

	logs, err := r.statusLogs(tx, astrologer.ID, profileStatusLogLimit)

	if err != nil {
		return nil, err
	}

	meta.StatusLogs = logs
	meta.Social = socialLinks(astrologer)

	return meta, nil
}

// profileStatusLogLimit bounds the status history the profile carries. The
// table grows with every availability toggle, so the whole list has no place
// in a profile payload.
const profileStatusLogLimit = 20

// joinAddress builds the one-line address the profile shows out of the three
// columns step 1 fills, skipping the ones the astrologer left blank.
func joinAddress(user models.User) string {

	parts := []string{}

	for _, part := range []string{
		user.AddressLine1,
		user.AddressLine2,
		user.Location,
	} {

		trimmed := strings.TrimSpace(part)

		if trimmed == "" {
			continue
		}

		parts = append(parts, trimmed)
	}

	return strings.Join(parts, ", ")
}

// firstWord is the leading word of a name — "Vijay" out of "Vijay Kumar".
func firstWord(value string) string {

	fields := strings.Fields(strings.TrimSpace(value))

	if len(fields) == 0 {
		return ""
	}

	return fields[0]
}

// socialLinks prefers the astrologer_social_links row, which is the table the
// admin panel writes, and falls back to the columns on the astrologers row.
// Either can be the populated one, depending on how the profile was created.
func socialLinks(astrologer *models.Astrologer) dto.SocialLinks {

	links := dto.SocialLinks{
		Instagram: astrologer.InstaProfileLink,
		Facebook:  astrologer.FacebookProfileLink,
		Youtube:   astrologer.YoutubeProfileLink,
		Telegram:  astrologer.TelegramProfileLink,
		Linkedin:  astrologer.LinkedinProfileLink,
		Website:   astrologer.WebsiteProfileLink,
	}

	for _, row := range astrologer.SocialLinks {

		if row.IsDelete {
			continue
		}

		links.Instagram = preferValue(row.Instagram, links.Instagram)
		links.Facebook = preferValue(row.Facebook, links.Facebook)
		links.Youtube = preferValue(row.Youtube, links.Youtube)
		links.Telegram = preferValue(row.Telegram, links.Telegram)
		links.Linkedin = preferValue(row.Linkedin, links.Linkedin)
		links.Website = preferValue(row.Website, links.Website)
	}

	return links
}

func preferValue(value string, fallback string) string {

	if strings.TrimSpace(value) != "" {
		return value
	}

	return fallback
}

// profileCompletion reports which steps are done.
//
// astrologer_profile_completion is written on submit, so it is missing for
// every profile still in progress. When there is no row the flags are computed
// from the data the astrologer has actually saved, which is what the screen
// wants either way, and the percentage comes from the astrologers row.
func (r *authRepository) profileCompletion(
	tx *gorm.DB,
	astrologer *models.Astrologer,
) (dto.ProfileCompletionResponse, error) {

	var stored models.AstrologerProfileCompletion

	err := tx.
		Where("astrologer_id = ?", astrologer.ID).
		First(&stored).Error

	if err == nil {

		return dto.ProfileCompletionResponse{
			BasicProfile:         stored.BasicProfile,
			Languages:            stored.Languages,
			ProfileImage:         stored.ProfileImage,
			Skills:               stored.Skills,
			Education:            stored.Education,
			Professional:         stored.Professional,
			Experience:           stored.Experience,
			Bank:                 stored.Bank,
			Documents:            stored.Documents,
			SocialLinks:          stored.SocialLinks,
			Availability:         stored.Availability,
			CompletionPercentage: stored.CompletionPercentage,
		}, nil
	}

	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return dto.ProfileCompletionResponse{}, err
	}

	// The preloads on GetLoggedInProfile already carry most of this, so the
	// flags are read off the loaded row rather than counted again.
	return dto.ProfileCompletionResponse{

		BasicProfile: strings.TrimSpace(astrologer.Name) != "" &&
			astrologer.BirthDate != nil,

		Languages:    len(astrologer.Languages) > 0,
		ProfileImage: strings.TrimSpace(astrologer.ProfileImage) != "",
		Skills:       len(astrologer.Skills) > 0,
		Education:    len(astrologer.Educations) > 0,
		Professional: len(astrologer.Professionals) > 0,
		Experience:   len(astrologer.Experiences) > 0,
		Bank:         len(astrologer.BankAccount) > 0,
		Documents:    len(astrologer.Documents) > 0,
		SocialLinks:  hasAnySocialLink(socialLinks(astrologer)),
		Availability: len(astrologer.Availability) > 0,

		CompletionPercentage: astrologer.CompletionPercentage,
	}, nil
}

func hasAnySocialLink(links dto.SocialLinks) bool {

	for _, link := range []string{
		links.Instagram,
		links.Facebook,
		links.Youtube,
		links.Telegram,
		links.Linkedin,
		links.Website,
	} {

		if strings.TrimSpace(link) != "" {
			return true
		}
	}

	return false
}

// statusLogs returns the astrologer's most recent status changes, newest
// first.
func (r *authRepository) statusLogs(
	tx *gorm.DB,
	astrologerID uint,
	limit int,
) ([]dto.StatusLogEntry, error) {

	var rows []models.AstrologerStatusLog

	err := tx.
		Where("astrologer_id = ?", astrologerID).
		Where("is_delete = ?", false).
		Order("started_at DESC, id DESC").
		Limit(limit).
		Find(&rows).Error

	if err != nil {
		return nil, err
	}

	entries := make([]dto.StatusLogEntry, 0, len(rows))

	for _, row := range rows {

		entry := dto.StatusLogEntry{
			ID:              row.ID,
			StatusType:      row.StatusType,
			PreviousStatus:  row.PreviousStatus,
			CurrentStatus:   row.CurrentStatus,
			Remarks:         row.Remarks,
			StartedAt:       formatLogTime(&row.StartedAt),
			EndedAt:         formatLogTime(row.EndedAt),
			DurationSeconds: row.DurationSeconds,
		}

		entries = append(entries, entry)
	}

	return entries, nil
}

// formatLogTime renders a log timestamp, or "" for one that was never set.
func formatLogTime(value *time.Time) string {

	if value == nil || value.IsZero() {
		return ""
	}

	return value.Format(time.RFC3339)
}

// resolveSkillNames returns the primary skill and the full skill list as
// comma-separated master names.
func (r *authRepository) resolveSkillNames(
	tx *gorm.DB,
	astrologerID uint,
	primaryColumn string,
	allColumn string,
) (string, string) {

	type skillRow struct {
		Name      string `gorm:"column:name"`
		IsPrimary bool   `gorm:"column:is_primary"`
	}

	var rows []skillRow

	tx.Table("astrologer_skills AS asx").
		Select("s.name AS name, asx.is_primary AS is_primary").
		Joins("JOIN skills s ON s.id = asx.skill_id").
		Where("asx.astrologer_id = ?", astrologerID).
		Order("asx.is_primary DESC, asx.id ASC").
		Scan(&rows)

	if len(rows) > 0 {

		names := make([]string, 0, len(rows))
		primary := ""

		for _, row := range rows {

			names = append(names, row.Name)

			if row.IsPrimary && primary == "" {
				primary = row.Name
			}
		}

		// No row flagged primary: the first selection stands in.
		if primary == "" {
			primary = names[0]
		}

		return primary, strings.Join(names, ", ")
	}

	return resolvedOrRaw(r.skillNames(tx, primaryColumn), primaryColumn),
		resolvedOrRaw(r.skillNames(tx, allColumn), allColumn)
}

// resolveLanguageNames returns the languages as comma-separated master names.
func (r *authRepository) resolveLanguageNames(
	tx *gorm.DB,
	astrologerID uint,
	column string,
) string {

	var names []string

	tx.Table("astrologer_languages AS al").
		Select("l.languageName").
		Joins("JOIN languages l ON l.id = al.language_id").
		Where("al.astrologer_id = ?", astrologerID).
		Order("al.id ASC").
		Pluck("l.languageName", &names)

	if len(names) > 0 {
		return strings.Join(names, ", ")
	}

	return resolvedOrRaw(r.languageNames(tx, column), column)
}

// skillNames resolves a comma-separated id list against the skills master.
func (r *authRepository) skillNames(tx *gorm.DB, ids string) string {

	values := idList(ids)

	if len(values) == 0 {
		return ""
	}

	var names []string

	tx.Table("skills").
		Where("id IN ?", values).
		Pluck("name", &names)

	return strings.Join(names, ", ")
}

// languageNames resolves a comma-separated id list against the languages
// master.
func (r *authRepository) languageNames(tx *gorm.DB, ids string) string {

	values := idList(ids)

	if len(values) == 0 {
		return ""
	}

	var names []string

	tx.Table("languages").
		Where("id IN ?", values).
		Pluck("languageName", &names)

	return strings.Join(names, ", ")
}

// idList splits a denormalised column into its ids, dropping anything that is
// not one — a column holding "Hindi" has no ids to look up, and passing that
// text to MySQL as an id would compare it as 0 and match nothing.
func idList(value string) []string {

	values := []string{}

	for _, part := range strings.Split(value, ",") {

		trimmed := strings.TrimSpace(part)

		if trimmed == "" || !isDigits(trimmed) {
			continue
		}

		values = append(values, trimmed)
	}

	return values
}

func isDigits(value string) bool {

	for _, char := range value {

		if char < '0' || char > '9' {
			return false
		}
	}

	return true
}

// resolvedOrRaw keeps a legacy column readable: showing the text it holds
// beats showing an empty field.
func resolvedOrRaw(resolved string, column string) string {

	if resolved != "" {
		return resolved
	}

	return strings.TrimSpace(column)
}

// ratingSummary returns the average rating and how many ratings it is over.
// A deleted review is excluded — it is no longer part of the astrologer's
// record.
func (r *authRepository) ratingSummary(tx *gorm.DB, astrologerID uint) (float64, int64) {

	var average float64
	var total int64

	query := func() *gorm.DB {
		return tx.Table("user_reviews").
			Where("astrologerId = ?", astrologerID).
			Where("isDelete = ?", false)
	}

	query().Select("IFNULL(AVG(rating),0)").Scan(&average)
	query().Count(&total)

	return average, total
}

// followerCount is how many users follow the astrologer.
func (r *authRepository) followerCount(tx *gorm.DB, astrologerID uint) int64 {

	var total int64

	tx.Table("astrologer_followers").
		Where("astrologerId = ?", astrologerID).
		Where("isActive = ?", true).
		Where("isDelete = ?", false).
		Count(&total)

	return total
}

// completedSessions counts the consultations the astrologer has finished —
// chats plus calls, the same tables and status the user-side listings count.
func (r *authRepository) completedSessions(tx *gorm.DB, astrologerID uint) int64 {

	var chats int64
	var calls int64

	tx.Table("chatrequest").
		Where("astrologerId = ?", astrologerID).
		Where("chatStatus = ?", "Completed").
		Count(&chats)

	tx.Table("callrequest").
		Where("astrologerId = ?", astrologerID).
		Where("callStatus = ?", "Completed").
		Count(&calls)

	return chats + calls
}
