package services_astrologer

import (
	config "astrology-api/configs"
	constants "astrology-api/constants"
	dto "astrology-api/dto_astrologer"
	dto_astrologer "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
	repositories "astrology-api/repositories_astrologer"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
)

type AstrologerService interface {
	validateProfileCompletion(completion *models.AstrologerProfileCompletion) error
	createOrUpdateProfileCompletion(astrologerID uint, step int) error
	// Step-1
	// SaveBasicProfile(userID uint, req dto.CreateAstrologerProfileRequest) error
	SaveBasicProfile(userID uint, req dto.CreateAstrologerProfileRequest) (*dto.BasicProfileResponse, error)

	// Step-2
	GetMasterLanguages(userID uint) ([]dto_astrologer.LanguageMasterResponse, error)

	// SaveLanguages(userID uint, req dto.SaveLanguagesRequest) error

	SaveLanguages(userID uint, req dto.SaveLanguagesRequest) (*dto.LanguageSaveResponse, error)

	// Step-3
	GetMasterSkills(userID uint) ([]dto_astrologer.SkillMasterResponse, error)

	// SaveSkills(userID uint, req dto.SaveSkillsRequest) error
	SaveSkills(userID uint, req dto.SaveSkillsRequest) (*dto.SkillSaveResponse, error)

	// Step-4
	SaveEducation(userID uint, req dto.EducationRequest) (*dto.EducationSaveResponse, error)

	UploadProfileImage(userID uint, file *multipart.FileHeader) (*dto.UploadProfileImageResponse, error)

	// UpdateProfileImage is the maintenance counterpart of UploadProfileImage.
	// It was implemented but missing from this interface, so PUT
	// /profile/update-image had to call the step-4 method instead.
	UpdateProfileImage(userID uint, file *multipart.FileHeader) (*dto.UpdateProfileImageResponse, error)

	// Step-5
	SaveProfessional(userID uint, req dto.ProfessionalRequest) (*dto.ProfessionalSaveResponse, error)

	// Step-6
	SaveExperience(userID uint, req dto.ExperienceRequest) (*dto.ExperienceSaveResponse, error)

	// Step-7

	SaveBankAccount(userID uint, req dto.BankRequest) (*dto.BankSaveResponse, error)

	// Step-8
	GetMasterDocumentTypes() ([]dto_astrologer.MasterDocumentResponse, error)

	UploadDocument(userID uint, req dto.UploadDocumentRequest, file *multipart.FileHeader) (*dto.UploadDocumentResponse, error)

	GetWorkingHours() ([]dto_astrologer.WorkingHourResponse, error)

	GetDeviceTypes() ([]dto_astrologer.DeviceTypeResponse, error)

	GetAvailability(userID uint) (*dto_astrologer.GetAvailabilityResponse, error)

	SaveAvailability(userID uint, req dto_astrologer.SaveAvailabilityRequest) (*dto_astrologer.SaveAvailabilityResponse, error)

	SubmitProfile(userID uint) (*dto_astrologer.SubmitProfileResponse, error)

	GetProfile(userID uint) (*dto.AstrologerProfileResponse, error)

	GetProfileDetails(userID uint) (*dto.AstrologersProfileResponse, error)

	GetProfileProgress(userID uint) (*dto.ProfileProgressResponse, error)

	ChangeStatus(userID uint, req dto_astrologer.ChangeStatusRequest) error

	DeleteAccount(userID uint) error

	UpdateProfile(userID uint, req dto.UpdateProfileRequest) (*dto.UpdateProfileResponse, error)

	DeleteProfileImage(userID uint) (*dto.DeleteProfileImageResponse, error)

	UpdateAvailability(userID uint, req dto_astrologer.SaveAvailabilityRequest) (*dto_astrologer.UpdateAvailabilityResponse, error)

	UpdateBankAccount(userID uint, req dto_astrologer.UpdateBankAccountRequest) (*dto_astrologer.UpdateBankAccountResponse, error)

	UpdateSkills(userID uint, req dto_astrologer.UpdateSkillsRequest) (*dto_astrologer.UpdateSkillsResponse, error)

	UpdateLanguages(userID uint, req dto_astrologer.UpdateLanguagesRequest) (*dto_astrologer.UpdateLanguagesResponse, error)

	UpdateEducation(userID uint, req dto.UpdateEducationRequest) (*dto.UpdateEducationResponse, error)

	UpdateProfessional(userID uint, req dto.UpdateProfessionalRequest) (*dto.UpdateProfessionalResponse, error)

	UpdateExperience(userID uint, req dto.UpdateExperienceRequest) (*dto.UpdateExperienceResponse, error)

	UpdateDocument(userID uint, req dto.UploadDocumentRequest, file *multipart.FileHeader) (*dto.UpdateDocumentResponse, error)
}

type astrologerService struct {
	repository repositories.AstrologerRepository
}

func NewAstrologerService(
	repository repositories.AstrologerRepository,
) AstrologerService {

	return &astrologerService{
		repository: repository,
	}
}

func (s *astrologerService) executeTransaction(

	fn func(tx *gorm.DB) error,

) error {

	tx := config.DB.Begin()

	if tx.Error != nil {
		return tx.Error
	}

	defer func() {

		if r := recover(); r != nil {

			tx.Rollback()

			panic(r)

		}

	}()

	if err := fn(tx); err != nil {

		tx.Rollback()

		return err

	}

	return tx.Commit().Error

}

func (s *astrologerService) getAstrologer(userID uint) (*models.Astrologer, error) {

	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {

		return nil, errors.New("astrologer not found")

	}

	return astrologer, nil

}

func (s *astrologerService) updateProgress(astrologer *models.Astrologer, step int) {
	if step > astrologer.CurrentStep {

		astrologer.CurrentStep = step
	}

}

func (s *astrologerService) calculateCompletion(astrologer *models.Astrologer) {

	if percentage, ok := constants.CompletionMap[astrologer.CurrentStep]; ok {
		astrologer.CompletionPercentage = percentage
	}
}

// updateProfileStatus tracks how far the ten-step flow has got.
//
// Finishing the tenth step means the profile is COMPLETE, not SUBMITTED. This
// used to write SUBMITTED the moment completion hit 100%, on any ordinary step
// save, which meant:
//
//   - a profile went to "submitted" without ever calling /profile/submit, so
//     lockProfileAfterSubmission never ran, no interview row was created and
//     admin_status was never set — it only looked submitted; and
//   - a REJECTED profile at 100% was pushed straight back to SUBMITTED by the
//     first correction the astrologer saved, re-locking the very edit they were
//     asked to make.
//
// Submission is now only ever done by SubmitProfile. A status that an admin owns
// is left alone entirely.
func (s *astrologerService) updateProfileStatus(astrologer *models.Astrologer) {

	switch astrologer.ProfileStatus {

	case constants.ProfileSubmitted,
		constants.ProfileUnderReview,
		constants.ProfileInterviewRound1,
		constants.ProfileInterviewRound2,
		constants.ProfileDocumentsVerified,
		constants.ProfileApproved:

		return
	}

	astrologer.IsProfileCompleted = astrologer.CompletionPercentage == 100

	// A rejected profile stays rejected while it is being corrected, so the app
	// can keep showing why it came back until it is resubmitted.
	if astrologer.ProfileStatus == constants.ProfileRejected {
		return
	}

	astrologer.ProfileStatus = constants.ProfileInProgress
}

func (s *astrologerService) saveProgress(

	tx *gorm.DB,

	astrologer *models.Astrologer,

) error {

	return tx.Save(astrologer).Error

}

func (s *astrologerService) SaveAvailability(userID uint, req dto_astrologer.SaveAvailabilityRequest) (*dto_astrologer.SaveAvailabilityResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	resp := &dto_astrologer.SaveAvailabilityResponse{}
	err := s.executeTransaction(func(tx *gorm.DB) error {
		// Validation
		if strings.TrimSpace(req.DeviceType) == "" {
			return errors.New("device type is required")
		}

		if req.DailyWorkingHours <= 0 {
			return errors.New("daily working hours is required")
		}

		if req.ChatRate < 0 {
			return errors.New("invalid chat rate")
		}

		if req.CallRate < 0 {
			return errors.New("invalid call rate")
		}

		if req.IsWorkingOnPlatform &&
			strings.TrimSpace(req.PlatformName) == "" {
			return errors.New("platform name is required")
		}
		// Step 9 comes first. Without this, saving availability raised
		// current_step to 10 and carried an astrologer past the document step
		// with KYC still outstanding.
		if err := s.ensureKYCDocumentsComplete(userID); err != nil {
			return err
		}
		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}
		// Update Astrologer Table
		astrologer.DeviceType = req.DeviceType
		astrologer.DailyHours = req.DailyWorkingHours
		astrologer.ChatRate = req.ChatRate
		astrologer.AudioCallRate = req.CallRate
		astrologer.VideoCallRate = req.VideoCallRate
		astrologer.Charge = req.CallRate
		astrologer.WorkingPlatform = req.IsWorkingOnPlatform
		astrologer.WorkingPlatformName = req.PlatformName
		astrologer.FreeChat = req.FreeChat
		astrologer.FreeCall = req.FreeCall
		astrologer.ChatStatus = "Completed"
		astrologer.ChatWaitTime = "Completed"
		astrologer.CallStatus = "Completed"
		astrologer.CallWaitTime = "Completed"

		astrologer.InstaProfileLink = req.SocialLinks.Instagram
		astrologer.FacebookProfileLink = req.SocialLinks.Facebook
		astrologer.YoutubeProfileLink = req.SocialLinks.Youtube
		astrologer.TelegramProfileLink = req.SocialLinks.Telegram
		astrologer.LinkedinProfileLink = req.SocialLinks.Linkedin
		astrologer.WebsiteProfileLink = req.SocialLinks.Website
		astrologer.Nameofplateform = req.PlatformName

		// Part 2.3.2
		_ = req.InterviewFrom
		_ = req.InterviewTo

		// Save Astrologer
		if err := s.repository.UpdateAstrologerAvailablity(tx, astrologer); err != nil {
			return err
		}

		// Continue Part 2.3.2
		if err := s.repository.DeleteAvailabilityByAstrologerID(tx, astrologer.ID); err != nil {
			return err
		}

		// Save Weekly Schedule
		for _, item := range req.Schedule {

			if strings.TrimSpace(item.Day) == "" {
				return errors.New("day is required")
			}

			if strings.TrimSpace(item.FromTime) == "" {
				return errors.New("from time is required")
			}

			if strings.TrimSpace(item.ToTime) == "" {
				return errors.New("to time is required")
			}

			availability := models.AstrologerAvailability{

				AstrologerID: astrologer.ID,

				Day: item.Day,

				FromTime: item.FromTime,

				ToTime: item.ToTime,

				SlotDuration: item.SlotDuration,

				Timezone: req.Timezone,

				IsBreak: item.IsBreak,

				IsActive: true,

				IsDelete: false,

				InterviewFrom: req.InterviewFrom,

				InterviewTo: req.InterviewTo,

				CreatedBy: userID,

				ModifiedBy: userID,
			}

			if err := s.repository.CreateAvailability(
				tx,
				&availability,
			); err != nil {

				return err
			}
		}

		// Validate Interview Time
		if req.InterviewFrom != "" && req.InterviewTo != "" {

			from, err := time.Parse("15:04", req.InterviewFrom)

			if err != nil {
				return errors.New("invalid interview_from time")
			}

			to, err := time.Parse("15:04", req.InterviewTo)

			if err != nil {
				return errors.New("invalid interview_to time")
			}

			if !from.Before(to) {
				return errors.New("interview_from must be earlier than interview_to")
			}
		}

		// Save / Update Social Links
		social, err := s.repository.GetSocialLinksByAstrologerID(
			astrologer.ID,
		)

		if err != nil {

			// Record not found → Create

			if errors.Is(err, gorm.ErrRecordNotFound) {

				newSocial := &models.AstrologerSocialLink{

					AstrologerID: astrologer.ID,

					Facebook: req.SocialLinks.Facebook,

					Instagram: req.SocialLinks.Instagram,

					Youtube: req.SocialLinks.Youtube,

					Telegram: req.SocialLinks.Telegram,

					Linkedin: req.SocialLinks.Linkedin,

					Website: req.SocialLinks.Website,

					IsActive: true,

					IsDelete: false,

					CreatedBy: userID,

					ModifiedBy: userID,
				}

				if err := s.repository.CreateSocialLinks(
					tx,
					newSocial,
				); err != nil {

					return err
				}

			} else {

				return err
			}

		} else {

			//----------------------------------
			// Update Existing Record
			//----------------------------------

			social.Facebook = req.SocialLinks.Facebook

			social.Instagram = req.SocialLinks.Instagram

			social.Youtube = req.SocialLinks.Youtube

			social.Telegram = req.SocialLinks.Telegram

			social.Linkedin = req.SocialLinks.Linkedin

			social.Website = req.SocialLinks.Website

			social.ModifiedBy = userID

			if err := s.repository.UpdateSocialLinks(
				tx,
				social,
			); err != nil {

				return err
			}
		}

		// Update Profile Progress
		s.updateProgress(
			astrologer,
			constants.StepAvailability,
		)

		s.calculateCompletion(astrologer)

		astrologer.CurrentStep = 10
		astrologer.CompletionPercentage = 100
		astrologer.IsProfileCompleted = true
		astrologer.ProfileStatus = constants.ProfileInProgress

		if err := s.repository.UpdateAstrologer(
			tx,
			astrologer,
		); err != nil {

			return err
		}

		// Update Progress Table
		progress := &models.AstrologerProfileProgress{
			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          10,
			CompletionPercentage: 100,
			ProfileCompleted:     true,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {

			return err
		}

		// Create Verification Log
		if err := s.createVerificationLog(
			tx,
			astrologer.ID,
			constants.ProfileInProgress,
			constants.ProfileSubmitted,
		); err != nil {

			return err
		}

		// Response
		resp.CurrentStep = 10

		resp.CompletionPercentage = 100

		resp.NextStep = 11

		resp.NextScreen = "Submit Profile"

		return nil

	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) validateSubmitProfile(
	progress *models.AstrologerProfileProgress,
) error {

	if progress == nil {
		return errors.New("profile progress not found")
	}

	// Already Submitted

	if progress.Submitted {
		return errors.New("profile already submitted")
	}

	// Last Step Completed
	if progress.CurrentStep != constants.StepAvailability {
		return fmt.Errorf(
			"please complete profile step %d before submitting",
			constants.StepAvailability,
		)
	}

	//------------------------------------------
	// Completion Percentage
	//------------------------------------------

	if progress.CompletionPercentage < 100 {
		return errors.New("profile completion must be 100%")
	}

	//------------------------------------------
	// Profile Completed
	//------------------------------------------

	if !progress.ProfileCompleted {
		return errors.New("profile is not completed")
	}

	//------------------------------------------
	// Admin Status Validation
	//------------------------------------------

	if progress.AdminStatus != constants.ProfileInProgress &&
		progress.AdminStatus != constants.ProfileNotSubmitted {
		return errors.New("profile cannot be submitted in current status")
	}

	return nil
}

func (s *astrologerService) validateSubmission(astrologer *models.Astrologer,
) error {

	if astrologer == nil {
		return errors.New("astrologer not found")
	}

	if strings.TrimSpace(astrologer.Name) == "" {
		return errors.New("basic profile is incomplete")
	}

	if strings.TrimSpace(astrologer.ProfileImage) == "" {
		return errors.New("profile image is required")
	}

	if astrologer.CurrentStep < constants.StepDocuments {
		return fmt.Errorf(
			"please complete all profile steps (current step %d of %d)",
			astrologer.CurrentStep,
			constants.StepDocuments,
		)
	}

	if astrologer.CompletionPercentage != 100 {
		return fmt.Errorf(
			"profile completion is %d%%",
			astrologer.CompletionPercentage,
		)
	}

	return nil
}

func (s *astrologerService) createVerificationLog(tx *gorm.DB, astrologerID uint, oldStatus string, newStatus string) error {

	log := models.AstrologerVerificationLog{

		AstrologerID: astrologerID,

		OldStatus: oldStatus,

		NewStatus: newStatus,
	}

	return tx.Create(&log).Error

}

// earliestBirthYear is the floor for a date of birth — enough to catch a
// mistyped year (1095, 2915) without putting an age limit on anyone.
const earliestBirthYear = 1900

// parseBirthDate reads a YYYY-MM-DD date of birth and rejects one that cannot
// be true: a future date, or a year outside a plausible range. The date is
// compared at day granularity against the local date, so today is accepted and
// tomorrow is not.
func parseBirthDate(value string) (*time.Time, error) {

	trimmed := strings.TrimSpace(value)

	birthDate, err := time.ParseInLocation("2006-01-02", trimmed, time.Local)

	if err != nil {
		return nil, errors.New("invalid birth date, expected format YYYY-MM-DD")
	}

	now := time.Now()
	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)

	if birthDate.After(today) {
		return nil, errors.New("date of birth cannot be a future date")
	}

	if birthDate.Year() < earliestBirthYear {
		return nil, fmt.Errorf(
			"date of birth must be on or after %d-01-01",
			earliestBirthYear,
		)
	}

	return &birthDate, nil
}

func (s *astrologerService) SaveBasicProfile(userID uint, req dto.CreateAstrologerProfileRequest) (*dto.BasicProfileResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	var resp *dto.BasicProfileResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {
		//-------------- Validation ------------------------
		if strings.TrimSpace(req.Name) == "" {
			return errors.New("name is required")
		}
		if strings.TrimSpace(req.Gender) == "" {
			return errors.New("gender is required")
		}
		if strings.TrimSpace(req.Location) == "" {
			return errors.New("city is required")
		}
		if strings.TrimSpace(req.State) == "" {
			return errors.New("state is required")
		}

		if strings.TrimSpace(req.MaritalStatus) == "" {
			return errors.New("marital status is required")
		}

		if strings.TrimSpace(req.PrimaryLanguage) == "" {
			return errors.New("primary language is required")
		}

		if strings.TrimSpace(req.PrimarySkills) == "" {
			return errors.New("primary skills are required")
		}

		if req.ExperienceYears <= 0 {
			return errors.New("experience years is required")
		}

		if req.DailyHours <= 0 {
			return errors.New("daily hours is required")
		}

		// astrologers.birthDate is NOT NULL, so an omitted date_of_birth reached
		// the insert as NULL and came back as a raw
		// "Error 1048 (23000): Column 'birthDate' cannot be null" — a database
		// error leaked to the client for what is an ordinary missing field.
		if strings.TrimSpace(req.DateOfBirth) == "" {
			return errors.New("date of birth is required")
		}

		//--------------- Birth Date ------------------------
		birthDate, err := parseBirthDate(req.DateOfBirth)

		if err != nil {
			return err
		}

		//---------------------------------------
		// Update USER table
		//---------------------------------------

		user, err := s.repository.GetUserTableByID(userID)
		if err != nil {
			return err
		}
		user.Name = req.Name
		user.Gender = req.Gender

		if birthDate != nil {

			user.BirthDate = birthDate.Format("2006-01-02")

		}

		user.Location = req.Location
		user.State = req.State
		user.Country = req.Country
		user.AddressLine1 = req.Address
		user.Pincode = req.Pincode
		user.Profile = req.ProfileImage

		// The users row is the source of truth for the verified email and
		// mobile, but the request carries them too and a registration that
		// stored one of them elsewhere would otherwise copy a blank onto the
		// astrologer. Fall back to the body, then write the result back to the
		// users row so both tables agree.
		if strings.TrimSpace(user.Email) == "" {
			user.Email = strings.TrimSpace(req.Email)
		}

		if strings.TrimSpace(user.ContactNo) == "" {
			user.ContactNo = strings.TrimSpace(req.Mobile)
		}

		if err := s.repository.UpdateUser(user); err != nil {
			return err
		}

		//---------------------------------------
		// Find Astrologer
		//---------------------------------------

		astrologer, err := s.repository.GetByUserID(userID)
		if errors.Is(err, gorm.ErrRecordNotFound) {

			astrologer = &models.Astrologer{
				UserID: userID,
			}

		} else if err != nil {

			return err
		}

		//---------------------------------------
		// Update Astrologer table
		//---------------------------------------

		astrologer.Name = req.Name

		// Never blank a stored identity: an empty users row would otherwise
		// wipe an email or mobile the astrologer already has on file.
		if strings.TrimSpace(user.Email) != "" {
			astrologer.Email = user.Email
		}

		if strings.TrimSpace(user.ContactNo) != "" {
			astrologer.ContactNo = user.ContactNo
		}

		// display_name has been a required request field with nowhere to go
		// until astrologers.displayName was added; fall back to the legal name
		// so the column is never blank.
		astrologer.DisplayName = strings.TrimSpace(req.DisplayName)

		if astrologer.DisplayName == "" {
			astrologer.DisplayName = strings.TrimSpace(req.Name)
		}

		astrologer.Gender = req.Gender
		astrologer.MaritalStatus = strings.TrimSpace(req.MaritalStatus)
		astrologer.BirthDate = birthDate
		astrologer.Bio = req.Bio
		astrologer.City = req.Location
		astrologer.CurrentCity = req.Location
		astrologer.State = req.State
		astrologer.Country = req.Country
		astrologer.ExperienceYears = req.ExperienceYears
		astrologer.DailyHours = req.DailyHours
		astrologer.WorkingPlatform = req.WorkingPlatform
		astrologer.WorkingPlatformName = req.WorkingPlatformName
		astrologer.LearnAstrology = req.LearnAstrologyFrom
		astrologer.ProfileImage = req.ProfileImage

		// Step 2 and step 3 own these two columns once they run. Step 1 seeds
		// them only when it was actually given a value, so re-saving basic
		// details does not erase the languages and skills already chosen.
		if strings.TrimSpace(req.PrimaryLanguage) != "" {
			astrologer.LanguageKnown = req.PrimaryLanguage
		}

		if strings.TrimSpace(req.PrimarySkills) != "" {
			astrologer.PrimarySkill = req.PrimarySkills
		}

		astrologer.ProfileStatus = constants.ProfileInProgress
		astrologer.IsProfileCompleted = false

		//---------------------------------------
		// Save Astrologer
		//
		// current_step and completion_percentage are set BEFORE the write. They
		// used to be applied to the struct afterwards, so the row was saved with
		// the old values and step 1 never persisted its progress: the astrologers
		// row stayed at current_step 0 / 0% even though the response said 1 / 10%.
		// Saving step 2 hid it again, because updateProgress then raised the step
		// to 2 anyway. The other nine steps already do it in this order.
		//---------------------------------------

		s.updateProgress(astrologer, constants.StepBasicProfile)
		s.calculateCompletion(astrologer)

		if astrologer.ID == 0 {

			if err := s.repository.Create(astrologer); err != nil {
				return err
			}

		} else {

			if err := s.repository.Update(astrologer); err != nil {
				return err
			}
		}

		progress := &models.AstrologerProfileProgress{

			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          astrologer.CurrentStep,
			CompletionPercentage: astrologer.CompletionPercentage,
			ProfileCompleted:     true,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
			IsCompleted:          false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}

		//---------------------------------------
		// Verification Log
		//---------------------------------------

		oldStatus := constants.ProfileIncomplete

		if astrologer.ID > 0 {

			oldStatus = constants.ProfileInProgress

		}

		if err := s.createVerificationLog(
			tx,
			astrologer.ID,
			oldStatus,
			constants.ProfileInProgress,
		); err != nil {
			return err
		}

		resp = &dto.BasicProfileResponse{
			CurrentStep:          astrologer.CurrentStep,
			CompletionPercentage: astrologer.CompletionPercentage,
			NextStep:             2,
			NextScreen:           "Save Astrologer Languages",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

// validateLanguageIDs checks a selection against the languages master and
// returns the ids in request order with duplicates dropped.
//
// astrologer_languages.language_id carries a foreign key onto languages, so an
// id that is not in the master used to fail at the insert and surface MySQL's
// raw "Error 1452 ... fk_language_master" to the app. Checking first turns that
// into a sentence a user can act on, and — because it runs before the delete
// the two write paths start with — leaves the stored selection untouched when
// the payload is rejected.
func (s *astrologerService) validateLanguageIDs(ids []uint) ([]uint, error) {

	if len(ids) == 0 {
		return nil, errors.New("please select at least one language")
	}

	unique := make([]uint, 0, len(ids))
	seen := make(map[uint]bool, len(ids))

	for _, id := range ids {

		if id == 0 {
			return nil, errors.New(
				"You have selected an undefined language. Please select a valid language and try again.",
			)
		}

		if seen[id] {
			continue
		}

		seen[id] = true
		unique = append(unique, id)
	}

	existingIDs, err := s.repository.FindExistingLanguageIDs(unique)

	if err != nil {
		return nil, err
	}

	exists := make(map[uint]bool, len(existingIDs))

	for _, id := range existingIDs {
		exists[id] = true
	}

	missing := []string{}

	for _, id := range unique {

		if !exists[id] {
			missing = append(missing, strconv.FormatUint(uint64(id), 10))
		}
	}

	if len(missing) > 0 {

		// The ids are named so the client's own logs point at the stale entry
		// in its cached master list, which is the usual cause.
		return nil, fmt.Errorf(
			"You have selected an undefined language (id %s). Please select a valid language and try again.",
			strings.Join(missing, ", "),
		)
	}

	return unique, nil
}

func (s *astrologerService) SaveLanguages(userID uint, req dto.SaveLanguagesRequest) (*dto.LanguageSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}
	resp := &dto.LanguageSaveResponse{}
	err := s.executeTransaction(func(tx *gorm.DB) error {
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Checked against the master before the delete below, so a bad id
		// neither reaches the foreign key nor clears the saved selection.
		selectedIDs, err := s.validateLanguageIDs(req.LanguageIDs)

		if err != nil {
			return err
		}

		if err := s.repository.DeleteLanguages(astrologer.ID); err != nil {
			return err
		}

		// Save Languages

		// astrologers.languageKnown is the denormalised copy the user-side
		// listing filters on with FIND_IN_SET, so it holds language *ids*, not
		// names. Step 1 seeded it with the free-text primary_language, which no
		// FIND_IN_SET could ever match; the real selection is made here.
		languageIDs := make([]string, 0, len(selectedIDs))

		for _, languageID := range selectedIDs {

			language := models.AstrologerLanguage{
				AstrologerID: astrologer.ID,
				LanguageID:   languageID,
			}

			if err := s.repository.CreateLanguage(&language); err != nil {
				return err
			}

			languageIDs = append(languageIDs, strconv.FormatUint(uint64(languageID), 10))
		}

		if len(languageIDs) > 0 {
			astrologer.LanguageKnown = strings.Join(languageIDs, ",")
		}

		s.updateProgress(astrologer, constants.StepLanguages)
		s.calculateCompletion(astrologer)
		s.updateProfileStatus(astrologer)
		if err := s.saveProgress(tx, astrologer); err != nil {
			return err
		}

		progress := &models.AstrologerProfileProgress{
			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          2,
			CompletionPercentage: 20,
			ProfileCompleted:     true,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
			IsCompleted:          false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}
		resp = &dto.LanguageSaveResponse{
			CurrentStep:          astrologer.CurrentStep,
			CompletionPercentage: astrologer.CompletionPercentage,
			NextStep:             3,
			NextScreen:           "Save Astrologer Skills",
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return resp, nil
}

func (s *astrologerService) SaveSkills(userID uint, req dto.SaveSkillsRequest) (*dto.SkillSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}
	err := s.executeTransaction(func(tx *gorm.DB) error {

		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}

		if err := s.repository.DeleteSkills(astrologer.ID); err != nil {
			return err
		}

		//----------------------------------------
		// Save Selected Skills
		//----------------------------------------

		// astrologers.primarySkill / .allSkill are the denormalised copies the
		// user-side listing reads. Only step 1 ever wrote them, and only if the
		// client happened to send primary_skills, so an astrologer who picked
		// their skills here left both columns empty.
		//
		// They hold skill *ids*: the listing filters allSkill with
		// FIND_IN_SET(skillID, ...) and getSkillNames resolves both columns
		// against skills.id. Names are produced on read, not stored here.
		var (
			primarySkillID string
			allSkillIDs    []string
		)

		for _, item := range req.Skills {

			skill := &models.AstrologerSkill{
				UserID:       astrologer.UserID,
				AstrologerID: astrologer.ID,
				SkillID:      item.SkillId,
				Experience:   item.Experience,
				IsPrimary:    item.IsPrimary,
			}

			if err := s.repository.CreateSkill(skill); err != nil {
				return err
			}

			id := strconv.FormatUint(uint64(item.SkillId), 10)

			allSkillIDs = append(allSkillIDs, id)

			// First flagged skill wins; if the client flags none, the first
			// skill in the list stands in so the column is never left blank.
			if item.IsPrimary && primarySkillID == "" {
				primarySkillID = id
			}
		}

		if primarySkillID == "" && len(allSkillIDs) > 0 {
			primarySkillID = allSkillIDs[0]
		}

		if len(allSkillIDs) > 0 {
			astrologer.PrimarySkill = primarySkillID
			astrologer.AllSkill = strings.Join(allSkillIDs, ",")
		}

		s.updateProgress(astrologer, constants.StepSkills)
		s.calculateCompletion(astrologer)
		s.updateProfileStatus(astrologer)
		if err := s.repository.Update(astrologer); err != nil {
			return err
		}

		//----------------------------------------
		// Update Progress Table
		//----------------------------------------

		progress := &models.AstrologerProfileProgress{

			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          astrologer.CurrentStep,
			CompletionPercentage: astrologer.CompletionPercentage,
			ProfileCompleted:     true,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
			IsCompleted:          false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return nil, err
	}

	return &dto.SkillSaveResponse{
		CurrentStep:          3,
		CompletionPercentage: 30,
		NextStep:             4,
		NextScreen:           "Upload Profile Image",
	}, nil

}
// earliestPassingYear rejects a typo'd year without guessing at anyone's age.
const earliestPassingYear = 1900

// educationBlockFilled reports whether the client sent anything at all in an
// education block. It is what makes PG optional: an untouched block is skipped
// entirely, while a half-filled one is validated like any other.
func educationBlockFilled(block dto.EducationDetails) bool {

	return strings.TrimSpace(block.Degree) != "" ||
		strings.TrimSpace(block.Institute) != "" ||
		strings.TrimSpace(block.FieldOfStudy) != "" ||
		strings.TrimSpace(block.Specialization) != "" ||
		block.PassingYear != 0
}

// validateEducationBlock enforces the mandatory set for one block — degree,
// institute, field_of_study and a plausible passing_year. label names the block
// in the error so a client can tell which half of the payload to fix.
func validateEducationBlock(label string, block dto.EducationDetails) error {

	if strings.TrimSpace(block.Degree) == "" {
		return fmt.Errorf("%s degree is required", label)
	}

	if strings.TrimSpace(block.Institute) == "" {
		return fmt.Errorf("%s institute is required", label)
	}

	if strings.TrimSpace(block.FieldOfStudy) == "" {
		return fmt.Errorf("%s field of study is required", label)
	}

	currentYear := time.Now().Year()

	if block.PassingYear == 0 {
		return fmt.Errorf("%s passing year is required", label)
	}

	if block.PassingYear < earliestPassingYear || block.PassingYear > currentYear {
		return fmt.Errorf(
			"%s passing year must be a valid year between %d and %d",
			label,
			earliestPassingYear,
			currentYear,
		)
	}

	return nil
}

// validateEducationRequest holds the rule that the UG block is mandatory and
// the PG block is not — but a PG block the client did start filling in has to
// be complete, since a degree with no institute is not worth storing.
func validateEducationRequest(ug dto.EducationDetails, pg dto.EducationDetails) error {

	if err := validateEducationBlock("UG", ug); err != nil {
		return err
	}

	if !educationBlockFilled(pg) {
		return nil
	}

	return validateEducationBlock("PG", pg)
}

func (s *astrologerService) SaveEducation(userID uint, req dto.EducationRequest) (*dto.EducationSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	resp := &dto.EducationSaveResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// UG is mandatory, PG is not. Checked before the delete below, so a
		// rejected payload cannot wipe the education already on file.
		if err := validateEducationRequest(req.UG, req.PG); err != nil {
			return err
		}

		// Delete Old Education
		if err := s.repository.DeleteEducations(astrologer.ID); err != nil {
			return err
		}

		// Save UG
		{
			education := &models.AstrologerEducation{
				AstrologerID:   astrologer.ID,
				EducationType:  "UG",
				Degree:         strings.TrimSpace(req.UG.Degree),
				Institute:      strings.TrimSpace(req.UG.Institute),
				Specialization: strings.TrimSpace(req.UG.FieldOfStudy),
				PassingYear:    req.UG.PassingYear,
			}

			if err := s.repository.CreateEducation(education); err != nil {
				return err
			}
		}

		// Save PG
		if educationBlockFilled(req.PG) {

			education := &models.AstrologerEducation{
				AstrologerID:   astrologer.ID,
				EducationType:  "PG",
				Degree:         strings.TrimSpace(req.PG.Degree),
				Institute:      strings.TrimSpace(req.PG.Institute),
				Specialization: strings.TrimSpace(req.PG.FieldOfStudy),
				PassingYear:    req.PG.PassingYear,
			}

			if err := s.repository.CreateEducation(education); err != nil {
				return err
			}
		}

		//-----------------------------------------
		// Save Highest Education into astrologers table
		//-----------------------------------------
		if strings.TrimSpace(req.PG.Degree) != "" {

			astrologer.HighestQualification = req.PG.Degree
			astrologer.Degree = req.PG.FieldOfStudy
			astrologer.College = req.PG.Institute

		} else if req.UG.Degree != "" {

			astrologer.HighestQualification = req.UG.Degree
			astrologer.Degree = req.UG.FieldOfStudy
			astrologer.College = req.UG.Institute
		}

		// Update Astrologer
		s.updateProgress(astrologer, constants.StepEducation)
		s.calculateCompletion(astrologer)
		s.updateProfileStatus(astrologer)

		if err := s.repository.Update(astrologer); err != nil {
			return err
		}

		// Update Progress Table
		progress := &models.AstrologerProfileProgress{
			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          astrologer.CurrentStep,
			CompletionPercentage: astrologer.CompletionPercentage,
			ProfileCompleted:     false,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
			IsCompleted:          false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}

		// Response
		resp.CurrentStep = astrologer.CurrentStep
		resp.CompletionPercentage = astrologer.CompletionPercentage
		resp.NextStep = 6
		resp.NextScreen = "Save Professional Details"

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

// professionalEntry is one row of professional history as the two write paths
// see it, so SaveProfessional and UpdateProfessional validate on identical
// rules even though their request DTOs name the fields differently.
type professionalEntry struct {
	Company          string
	Designation      string
	ExperienceYears  float64
	StartYear        int
	EndYear          int
	CurrentlyWorking *bool
}

// earliestProfessionalYear rejects a typo'd year (19 or 219) without guessing
// at anyone's actual career length.
const earliestProfessionalYear = 1900

// validateExperienceYears checks the year pair on one experience row.
//
// Unlike the professional block, both years are optional — the app has been
// sending experience with nothing but a title and a duration — so a zero is
// "not given" rather than an error. Whatever is given has to be a real year
// that is not in the future, and the pair has to be in order.
func validateExperienceYears(startYear int, endYear int) error {

	currentYear := time.Now().Year()

	for label, year := range map[string]int{
		"start year": startYear,
		"end year":   endYear,
	} {

		if year == 0 {
			continue
		}

		if year < earliestProfessionalYear || year > currentYear {
			return fmt.Errorf(
				"%s must be a valid year between %d and %d",
				label,
				earliestProfessionalYear,
				currentYear,
			)
		}
	}

	if startYear != 0 && endYear != 0 && endYear < startYear {
		return errors.New("end year cannot be earlier than the start year")
	}

	return nil
}

// validateProfessionalEntry enforces the mandatory set for a professional row:
// company, designation, both years and currently_working, with the years held
// inside a plausible range that ends at the current year — nobody's employment
// finishes in the future.
func validateProfessionalEntry(entry professionalEntry) error {

	if strings.TrimSpace(entry.Company) == "" {
		return errors.New("company name is required")
	}

	if strings.TrimSpace(entry.Designation) == "" {
		return errors.New("designation is required")
	}

	// A missing key, not a false value: CurrentlyWorking is a *bool precisely
	// so "currently_working": false passes and an absent key does not.
	if entry.CurrentlyWorking == nil {
		return errors.New("currently_working is required")
	}

	if entry.ExperienceYears < 0 {
		return errors.New("experience years cannot be negative")
	}

	currentYear := time.Now().Year()

	if entry.StartYear == 0 {
		return errors.New("start year is required")
	}

	if entry.EndYear == 0 {
		return errors.New("end year is required")
	}

	if entry.StartYear < earliestProfessionalYear {
		return fmt.Errorf(
			"start year must be a valid year between %d and %d",
			earliestProfessionalYear,
			currentYear,
		)
	}

	if entry.StartYear > currentYear {
		return fmt.Errorf(
			"start year cannot be greater than the current year (%d)",
			currentYear,
		)
	}

	if entry.EndYear < earliestProfessionalYear {
		return fmt.Errorf(
			"end year must be a valid year between %d and %d",
			earliestProfessionalYear,
			currentYear,
		)
	}

	if entry.EndYear > currentYear {
		return fmt.Errorf(
			"end year cannot be greater than the current year (%d)",
			currentYear,
		)
	}

	if entry.EndYear < entry.StartYear {
		return errors.New("end year cannot be earlier than the start year")
	}

	return nil
}

func (s *astrologerService) SaveProfessional(userID uint, req dto.ProfessionalRequest) (*dto.ProfessionalSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	resp := &dto.ProfessionalSaveResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Delete old records
		if err := s.repository.DeleteProfessionalByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		// Save all professional records
		for _, p := range req.Professionals {

			entry := professionalEntry{
				Company:          p.Company(),
				Designation:      p.Role(),
				ExperienceYears:  p.ExperienceYears,
				StartYear:        p.StartYear,
				EndYear:          p.EndYear,
				CurrentlyWorking: p.CurrentlyWorking,
			}

			if err := validateProfessionalEntry(entry); err != nil {
				return err
			}

			pro := models.AstrologerProfessional{

				AstrologerID: astrologer.ID,

				CurrentCompany: strings.TrimSpace(entry.Company),

				Occupation: strings.TrimSpace(entry.Designation),

				ExperienceYears: p.ExperienceYears,

				StartYear: p.StartYear,

				EndYear: p.EndYear,

				CurrentlyWorking: *p.CurrentlyWorking,
			}

			if err := s.repository.CreateProfessional(tx, &pro); err != nil {
				return err
			}
		}

		// Update profile progress
		s.updateProgress(astrologer, 6)

		s.calculateCompletion(astrologer)

		s.updateProfileStatus(astrologer)

		if err := s.saveProgress(tx, astrologer); err != nil {
			return err
		}

		resp.CurrentStep = astrologer.CurrentStep
		resp.CompletionPercentage = astrologer.CompletionPercentage
		resp.NextStep = 7
		resp.NextScreen = "Save Experience Details"

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) SaveExperience(userID uint, req dto.ExperienceRequest) (*dto.ExperienceSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	resp := &dto.ExperienceSaveResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		if len(req.Experience) == 0 {
			return errors.New("experience details are required")
		}

		// Delete Old Experience
		if err := s.repository.DeleteExperienceByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		fmt.Printf("Experience Request : %+v\n", req)
		fmt.Println("Total Experience :", len(req.Experience))

		// Save Experience
		for _, item := range req.Experience {

			fmt.Printf("Saving Experience : %+v\n", item)

			// Title is mandatory. The binding tag rejects a missing key; this
			// also rejects one that is present but blank, and matches the
			// message UpdateExperience already returns.
			title := strings.TrimSpace(item.Title)

			if title == "" {
				return errors.New("title is required")
			}

			if item.ExperienceYears < 0 {
				return errors.New("experience years cannot be negative")
			}

			if err := validateExperienceYears(
				item.StartYear,
				item.EndYear,
			); err != nil {
				return err
			}

			exp := models.AstrologerExperience{
				AstrologerID:    astrologer.ID,
				Title:           title,
				ExperienceYears: item.ExperienceYears,
				StartYear:       item.StartYear,
				EndYear:         item.EndYear,
				IsCurrent:       item.Working(),
				Description:     item.Description,
			}

			if err := s.repository.CreateExperience(
				tx,
				&exp,
			); err != nil {

				return err
			}
		}

		// Update Astrologer Table
		s.updateProgress(astrologer, 7)
		s.calculateCompletion(astrologer)
		s.updateProfileStatus(astrologer)
		if err := s.saveProgress(tx, astrologer); err != nil {
			return err
		}
		// Update Progress Table
		progress := &models.AstrologerProfileProgress{
			UserID:               astrologer.UserID,
			AstrologerID:         astrologer.ID,
			CurrentStep:          7,
			CompletionPercentage: astrologer.CompletionPercentage,
			ProfileCompleted:     false,
			Submitted:            false,
			AdminStatus:          constants.ProfileInProgress,
			IsCompleted:          false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}

		// Response
		resp.CurrentStep = 7
		resp.CompletionPercentage = astrologer.CompletionPercentage
		resp.NextStep = 8
		resp.NextScreen = "Save Bank Account"
		return nil
	})
	if err != nil {
		return nil, err
	}
	return resp, nil
}

func (s *astrologerService) SaveBankAccount(userID uint, req dto.BankRequest) (*dto.BankSaveResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	resp := &dto.BankSaveResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		//--------------------------------
		// Get Astrologer
		//--------------------------------

		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		//--------------------------------
		// Delete Old Bank Details
		//--------------------------------

		if err := s.repository.DeleteBankAccountByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		//--------------------------------
		// Save Bank Details
		//--------------------------------

		bank := models.AstrologerBankAccount{
			UserId:            astrologer.UserID,
			AstrologerID:      astrologer.ID,
			AccountHolderName: req.AccountHolderName,
			BankName:          req.BankName,
			BranchName:        req.BranchName,
			AccountNumber:     req.AccountNumber,
			IFSCCode:          req.IFSCCode,
			UPIID:             req.UPIID,
		}

		if err := s.repository.CreateBankAccount(
			tx,
			&bank,
		); err != nil {
			return err
		}

		//--------------------------------
		// Update Progress
		//--------------------------------

		s.updateProgress(astrologer, 8)

		s.calculateCompletion(astrologer)

		s.updateProfileStatus(astrologer)

		if err := s.saveProgress(tx, astrologer); err != nil {
			return err
		}

		//--------------------------------
		// Update Progress Table
		//--------------------------------

		progress := &models.AstrologerProfileProgress{

			UserID: astrologer.UserID,

			AstrologerID: astrologer.ID,

			CurrentStep: astrologer.CurrentStep,

			CompletionPercentage: astrologer.CompletionPercentage,

			ProfileCompleted: false,

			Submitted: false,

			AdminStatus: constants.ProfileInProgress,

			IsCompleted: false,
		}

		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}

		//--------------------------------
		// Response
		//--------------------------------

		resp.CurrentStep = astrologer.CurrentStep

		resp.CompletionPercentage = astrologer.CompletionPercentage

		resp.NextStep = 9

		resp.NextScreen = "Upload Documents"

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) SubmitProfile(userID uint) (*dto_astrologer.SubmitProfileResponse, error) {

	response := &dto_astrologer.SubmitProfileResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Check Profile Editable
		if err := s.validateProfileEditable(astrologer); err != nil {
			return err
		}

		// KYC is a submission requirement, not just a step counter: a row left
		// at step 10 by an earlier build could otherwise be submitted with
		// documents missing.
		if err := s.ensureKYCDocumentsComplete(userID); err != nil {
			return err
		}

		// Get Profile Progress
		progress, err := s.repository.GetProfileProgressByAstrologerID(astrologer.ID)

		if err != nil {
			return errors.New("profile progress not found")
		}

		// Validate Progress
		if err := s.validateSubmitProfile(progress); err != nil {
			return err
		}

		// Create Profile Completion
		if err := s.createProfileCompletion(astrologer.ID); err != nil {
			return err
		}

		// Lock Astrologer Profile
		if err := s.lockProfileAfterSubmission(astrologer); err != nil {
			return err
		}

		// Update Progress Table
		now := time.Now()
		progress.Submitted = true
		progress.SubmittedAt = &now
		progress.AdminStatus = constants.AdminPending
		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}
		completion, err := s.repository.GetProfileCompletionByAstrologerID(astrologer.ID)
		if err != nil {
			return err
		}
		if err := s.validateProfileCompletion(completion); err != nil {
			return err
		}
		// Verification Log
		if err := s.createVerificationLog(tx, astrologer.ID, constants.ProfileSubmitted, "Astrologer profile submitted for admin approval"); err != nil {
			return err
		}
		progress.Submitted = true
		progress.SubmittedAt = &now
		progress.AdminStatus = constants.AdminPending
		if err := s.repository.UpdateProfileProgress(progress); err != nil {
			return err
		}
		// Interview Round 1

		var interview models.AstrologerInterview

		err = tx.Where("user_id = ?", userID).First(&interview).Error

		if errors.Is(err, gorm.ErrRecordNotFound) {

			interview = models.AstrologerInterview{
				UserID:      userID,
				RoundNo:     1,
				Result:      constants.InterviewPending,
				Remarks:     "",
				ScheduledAt: nil,
				Interviewer: 0,
			}

			if err := tx.Create(&interview).Error; err != nil {
				return err
			}

		} else if err != nil {
			return err
		}

		// Response
		response.ProfileStatus = constants.ProfileSubmitted
		response.VerificationStatus = constants.AdminPending
		response.AdminStatus = constants.AdminPending
		response.CurrentStep = constants.StepAvailability
		response.CompletionPercentage = 100
		response.NextStep = 11
		response.NextScreen = "Verification Pending"
		response.CanEdit = false
		response.CanLogin = false
		response.Message = "Your profile has been submitted successfully and is waiting for admin verification."

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

// validateProfileEditable reports whether the ten-step profile flow is still
// open for this astrologer.
//
// ProfileSubmitted was missing from the blocked set, so a profile that had been
// submitted but not yet picked up by an admin could be submitted a second time.
// REJECTED stays editable on purpose: that is how an astrologer fixes what the
// admin sent back.
func (s *astrologerService) validateProfileEditable(astrologer *models.Astrologer) error {

	if astrologer == nil {
		return nil
	}

	switch astrologer.ProfileStatus {

	case constants.ProfileApproved,
		constants.ProfileSubmitted,
		constants.ProfileUnderReview,
		constants.ProfileInterviewRound1,
		constants.ProfileInterviewRound2,
		constants.ProfileDocumentsVerified:

		return errors.New("profile has already been submitted and cannot be edited")

	case constants.ProfileRejected:
		return nil

	default:
		return nil
	}
}

// ensureStepEditable guards the ten create-profile steps.
//
// Only SubmitProfile checked this before, so an astrologer whose profile was
// already under review — or already approved — could still rewrite every step
// underneath the admin. A missing astrologer row means step 1 has not been saved
// yet, which is exactly when these endpoints are supposed to be callable.
func (s *astrologerService) ensureStepEditable(userID uint) error {

	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}

		return err
	}

	return s.validateProfileEditable(astrologer)
}

// ensureUpdateAllowed guards the maintenance endpoints, the PUT /update-* set.
//
// These stay open to an APPROVED astrologer, who has to be able to keep a live
// profile current, but are closed while the profile sits with an admin: editing
// it mid-review would change what is being reviewed.
func (s *astrologerService) ensureUpdateAllowed(userID uint) error {

	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}

		return err
	}

	switch astrologer.ProfileStatus {

	case constants.ProfileSubmitted,
		constants.ProfileUnderReview,
		constants.ProfileInterviewRound1,
		constants.ProfileInterviewRound2,
		constants.ProfileDocumentsVerified:

		return errors.New("profile is under review and cannot be edited")

	default:
		return nil
	}
}

func (s *astrologerService) lockProfileAfterSubmission(astrologer *models.Astrologer) error {

	now := time.Now()

	astrologer.ProfileStatus = constants.ProfileSubmitted
	astrologer.AdminStatus = constants.AdminPending
	astrologer.VerificationStatus = constants.AdminPending
	astrologer.CurrentStep = constants.StepAvailability
	astrologer.CompletionPercentage = 100
	astrologer.IsProfileCompleted = true
	astrologer.IsActive = true
	astrologer.IsAvailable = 0
	astrologer.SubmittedAt = now

	User, err := s.repository.GetUserTableByID(astrologer.UserID)
	if err != nil {
		return err
	}
	User.IsActive = true
	User.UserRole = "RoleAstrologer"
	User.ProfileCompleted = true
	User.AstrologerProfileStatus = constants.ProfileSubmitted
	User.LoginAllowed = false

	if err := s.repository.UpdateUser(User); err != nil {
		return err
	}

	return s.repository.Update(astrologer)
}

func (s *astrologerService) submitProfileTransaction(tx *gorm.DB, userID uint) (*models.Astrologer, error) {

	//----------------------------------------
	// Get Astrologer
	//----------------------------------------

	astrologer, err := s.repository.GetByUserID(userID)
	if err != nil {
		return nil, err
	}

	//----------------------------------------
	// Validate Current Status
	//----------------------------------------

	switch astrologer.ProfileStatus {

	case constants.ProfileApproved:
		return nil, errors.New("profile already approved")

	case constants.ProfileSubmitted:
		return nil, errors.New("profile already submitted")

	case constants.ProfileUnderReview:
		return nil, errors.New("profile is under review")
	}

	//----------------------------------------
	// Validate Completion
	//----------------------------------------

	completion, err :=
		s.repository.GetProfileCompletionByAstrologerID(
			astrologer.ID,
		)

	if err != nil {
		return nil, err
	}

	if err := s.validateProfileCompletion(completion); err != nil {
		return nil, err
	}

	//----------------------------------------
	// Refresh Completion Record
	//----------------------------------------

	if err := s.createProfileCompletion(
		astrologer.ID,
	); err != nil {

		return nil, err
	}

	//----------------------------------------
	// Update Astrologer
	//----------------------------------------

	now := time.Now()

	astrologer.CurrentStep = constants.StepAvailability

	astrologer.CompletionPercentage = 100

	astrologer.IsProfileCompleted = true

	astrologer.ProfileStatus = constants.ProfileSubmitted

	astrologer.SubmittedAt = now

	if err := tx.Save(astrologer).Error; err != nil {
		return nil, err
	}

	//----------------------------------------
	// Update Progress
	//----------------------------------------

	progress, err :=
		s.repository.GetProgressByAstrologerID(astrologer.ID)

	if err != nil {

		progress = &models.AstrologerProgress{

			UserID: userID,

			AstrologerID: astrologer.ID,

			CurrentStep: constants.StepAvailability,

			CompletionPercentage: 100,

			ProfileCompleted: true,

			Submitted: true,

			SubmittedAt: &now,

			AdminStatus: constants.AdminPending,
		}

		if err := tx.Create(progress).Error; err != nil {
			return nil, err
		}

	} else {

		progress.CurrentStep = constants.StepAvailability

		progress.CompletionPercentage = 100

		progress.ProfileCompleted = true

		progress.Submitted = true

		progress.SubmittedAt = &now

		progress.AdminStatus = constants.AdminPending

		if err := tx.Save(progress).Error; err != nil {
			return nil, err
		}
	}

	return astrologer, nil
}

func (s *astrologerService) lockAstrologerProfile(astrologer *models.Astrologer) error {

	astrologer.ProfileStatus = constants.ProfileSubmitted

	astrologer.IsProfileCompleted = true

	astrologer.CurrentStep = constants.StepAvailability

	astrologer.CompletionPercentage = 100

	astrologer.IsAvailable = 0

	return s.repository.Update(astrologer)
}

func (s *astrologerService) GetProfile(userID uint) (*dto.AstrologerProfileResponse, error) {

	astrologer, err := s.repository.GetByUserID(userID)
	if err != nil {
		return nil, err
	}

	response := &dto.AstrologerProfileResponse{
		ID:                   astrologer.ID,
		Name:                 astrologer.Name,
		Email:                astrologer.Email,
		ContactNo:            astrologer.ContactNo,
		ProfileImage:         astrologer.ProfileImage,
		Gender:               astrologer.Gender,
		City:                 astrologer.City,
		State:                astrologer.State,
		Country:              astrologer.Country,
		ExperienceYears:      astrologer.ExperienceYears,
		DailyHours:           astrologer.DailyHours,
		ProfileStatus:        astrologer.ProfileStatus,
		CompletionPercentage: astrologer.CompletionPercentage,
		CurrentStep:          astrologer.CurrentStep,
		IsProfileCompleted:   astrologer.IsProfileCompleted,
		Languages:            []string{},
		Skills:               []string{},
	}

	// Load Languages
	languages, err := s.repository.GetLanguagesByAstrologerID(astrologer.ID)
	if err == nil {
		response.Languages = languages
	}

	// Load Skills
	skills, err := s.repository.GetSkillsByAstrologerID(astrologer.ID)
	if err == nil {
		response.Skills = skills
	}

	return response, nil
}

func boolPtr(v bool) *bool {
	return &v
}

func (s *astrologerService) createOrUpdateProfile(tx *gorm.DB, astrologer *models.Astrologer) error {
	if astrologer.ID == 0 {
		return s.repository.Create(astrologer)
	}
	return s.repository.Update(astrologer)

}

func timePtr(t time.Time) *time.Time {

	return &t

}

func (s *astrologerService) UploadProfileImage(userID uint, file *multipart.FileHeader) (*dto.UploadProfileImageResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errors.New("profile image is required")
	}

	ext := strings.ToLower(filepath.Ext(file.Filename))
	allowed := map[string]bool{
		".jpg":  true,
		".jpeg": true,
		".png":  true,
		".webp": true,
	}

	if !allowed[ext] {
		return nil, errors.New("only jpg, jpeg, png and webp allowed")
	}

	// Max 5 MB

	if file.Size > 5*1024*1024 {

		return nil, errors.New("image size must be less than 5 MB")

	}

	// Get Astrologer

	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {

		return nil, err

	}

	// Create Folder

	uploadDir := "uploads/astrologers/profile"

	if _, err := os.Stat(uploadDir); os.IsNotExist(err) {

		err = os.MkdirAll(uploadDir, os.ModePerm)

		if err != nil {

			return nil, err

		}
	}

	// Unique File Name

	fileName := fmt.Sprintf(

		"astro_%d_%d%s",

		userID,

		time.Now().Unix(),

		ext,
	)

	dst := filepath.Join(uploadDir, fileName)

	// Open Uploaded File

	src, err := file.Open()

	if err != nil {

		return nil, err

	}

	defer src.Close()

	// Create Destination

	out, err := os.Create(dst)

	if err != nil {

		return nil, err

	}

	defer out.Close()

	// Copy

	_, err = io.Copy(out, src)

	if err != nil {

		return nil, err

	}

	// Save Database

	// Relative path stored in DB
	imagePath := filepath.ToSlash(filepath.Join(
		"uploads",
		"astrologers",
		"profile",
		fileName,
	))
	baseURL := "https://api.astrology.togwe.com" // Read from .env
	imageURL := fmt.Sprintf("%s/%s", strings.TrimRight(baseURL, "/"), imagePath)

	astrologer.ProfileImage = imageURL

	s.updateProgress(astrologer, 4)

	s.calculateCompletion(astrologer)

	s.updateProfileStatus(astrologer)

	if err := s.repository.Update(astrologer); err != nil {
		return nil, err
	}

	//---------------------------------------
	// Update Progress Table
	//---------------------------------------

	progress := &models.AstrologerProfileProgress{
		UserID:               astrologer.UserID,
		AstrologerID:         astrologer.ID,
		CurrentStep:          4,
		CompletionPercentage: 40,
		ProfileCompleted:     false,
		Submitted:            false,
		AdminStatus:          constants.ProfileInProgress,
		IsCompleted:          false,
	}

	if err := s.repository.UpdateProfileProgress(progress); err != nil {
		return nil, err
	}

	return &dto.UploadProfileImageResponse{
		ProfileImage:         imageURL,
		CurrentStep:          4,
		CompletionPercentage: astrologer.CompletionPercentage,
		NextStep:             5,
		NextScreen:           "Save Education Details",
	}, nil

}

func (s *astrologerService) GetProfileDetails(userID uint) (*dto.AstrologersProfileResponse, error) {

	astrologer, err := s.repository.GetByUserID(userID)
	if err != nil {
		return nil, err
	}

	profile, err := s.repository.GetProfileDetails(astrologer.ID)
	if err != nil {
		return nil, err
	}

	response := &dto.AstrologersProfileResponse{}

	//-----------------------------------
	// Basic Profile
	//-----------------------------------

	response.ID = profile.ID
	response.Name = profile.Name
	response.ProfileImage = profile.ProfileImage
	response.Gender = profile.Gender
	if profile.BirthDate != nil {
		response.DateOfBirth = profile.BirthDate.Format("2006-01-02")
	}
	response.CurrentStep = profile.CurrentStep
	response.ProfileStatus = profile.ProfileStatus
	response.CompletionPercentage = profile.CompletionPercentage
	response.IsProfileCompleted = profile.IsProfileCompleted

	//-----------------------------------
	// Languages
	//-----------------------------------

	response.Languages = mapLanguages(profile.Languages)

	//-----------------------------------
	// Skills
	//-----------------------------------

	response.Skills = mapSkills(profile.Skills)

	//-----------------------------------
	// Education
	//-----------------------------------

	response.Educations = mapEducations(profile.Educations)

	//-----------------------------------
	// Professional
	//-----------------------------------

	response.Professionals = mapProfessionals(profile.Professionals)

	//-----------------------------------
	// Experience
	//-----------------------------------

	response.Experiences = mapExperiences(profile.Experiences)

	//-----------------------------------
	// Bank
	//-----------------------------------

	response.BankAccounts = mapBanks(profile.BankAccount)

	//-----------------------------------
	// Documents
	//-----------------------------------

	response.Documents = mapDocuments(profile.Documents)

	//-----------------------------------
	// Progress
	//-----------------------------------

	response.Progress = mapProgress(profile.Progress)

	//-----------------------------------
	// Verification Log
	//-----------------------------------

	response.VerificationLogs = mapVerificationLogs(profile.VerificationLogs)

	return response, nil
}

func mapLanguages(languages []models.AstrologerLanguage) []dto.LanguageResponse {

	result := make([]dto.LanguageResponse, 0)

	for _, l := range languages {

		result = append(result, dto.LanguageResponse{

			ID: l.LanguageID,

			Name: l.Language.LanguageName,
		})

	}

	return result
}

func mapSkills(skills []models.AstrologerSkill) []dto.SkillResponse {

	result := make([]dto.SkillResponse, 0)

	for _, s := range skills {

		result = append(result, dto.SkillResponse{

			ID: s.SkillID,

			Name: s.Skill.Name,

			Experience: s.Experience,
		})

	}

	return result
}

func mapProfessionals(professionals []models.AstrologerProfessional) []dto.ProfessionalResponse {

	result := make([]dto.ProfessionalResponse, 0)

	for _, p := range professionals {

		result = append(result, dto.ProfessionalResponse{

			ID: p.ID,

			Occupation: p.Occupation,

			EmploymentType: p.EmploymentType,

			CurrentCompany: p.CurrentCompany,

			ConsultationMode: p.ConsultationMode,

			JoiningDate: p.JoiningDate,

			ExperienceYears: p.ExperienceYears,

			ExperienceCertificate: p.ExperienceCertificate,
		})

	}

	return result
}

func mapExperiences(experiences []models.AstrologerExperience) []dto.ExperienceResponse {

	result := make([]dto.ExperienceResponse, 0)

	for _, e := range experiences {

		result = append(result, dto.ExperienceResponse{

			ID: e.ID,

			Title: e.Title,

			Organization: e.Organization,

			StartDate: e.StartDate,

			EndDate: e.EndDate,

			Description: e.Description,

			Certificate: e.Certificate,

			ExperienceYears: e.ExperienceYears,

			StartYear: e.StartYear,

			EndYear: e.EndYear,

			CurrentlyWorking: e.IsCurrent,
		})

	}

	return result
}

func mapBanks(banks []models.AstrologerBankAccount) []dto.BankResponse {

	result := make([]dto.BankResponse, 0)

	for _, b := range banks {

		result = append(result, dto.BankResponse{
			ID:                 b.ID,
			AccountHolderName:  b.AccountHolderName,
			BankName:           b.BankName,
			BranchName:         b.BranchName,
			AccountNumber:      b.AccountNumber,
			IFSCCode:           b.IFSCCode,
			UPIID:              b.UPIID,
			VerificationStatus: b.VerificationStatus,
		})
	}

	return result
}

func mapDocuments(documents []models.AstrologerDocument) []dto.DocumentResponse {

	result := make([]dto.DocumentResponse, 0)

	for _, d := range documents {

		result = append(result, dto.DocumentResponse{

			ID: d.ID,

			DocumentType: d.DocumentType,

			DocumentNumber: d.DocumentNumber,

			FilePath: d.FilePath,

			VerificationStatus: d.VerificationStatus,
		})

	}

	return result
}

func mapProgress(progress []models.AstrologerProfileProgress) []dto.ProgressResponse {

	result := make([]dto.ProgressResponse, 0)

	for _, p := range progress {

		result = append(result, dto.ProgressResponse{

			CurrentStep: p.CurrentStep,

			ProfileCompleted: p.ProfileCompleted,

			SubmittedAt: p.SubmittedAt,
		})

	}

	return result
}

func mapVerificationLogs(logs []models.AstrologerVerificationLog) []dto.VerificationLogResponse {

	result := make([]dto.VerificationLogResponse, 0)

	for _, l := range logs {

		result = append(result, dto.VerificationLogResponse{

			OldStatus: l.OldStatus,

			NewStatus: l.NewStatus,

			Remarks: l.Remarks,

			ChangedBy: l.ChangedBy,

			CreatedAt: l.CreatedAt,
		})

	}

	return result
}

func mapEducations(
	educations []models.AstrologerEducation,
) []dto.EducationResponse {

	result := make([]dto.EducationResponse, 0)

	for _, e := range educations {

		result = append(result, dto.EducationResponse{

			ID: e.ID,

			Degree: e.Degree,

			Specialization: e.Specialization,

			Institute: e.Institute,

			BoardUniversity: e.BoardUniversity,

			PassingYear: e.PassingYear,

			Certificate: e.Certificate,

			Status: e.Status,
		})

	}

	return result
}

// kycDocumentGroup is one mandatory KYC requirement on step 9, named by the
// master_type that /astrologer/master/document-types groups its codes under.
// Any single active code under that master_type satisfies the group: one side
// of the Aadhaar is enough, and either a cancelled cheque or a passbook.
//
// master_document_types marks each code is_required = 1 on its own and so
// cannot express that OR, which is why the grouping lives here. The codes
// themselves are not listed — they are read from the master table, so a code
// added or retired there is picked up without touching this file.
type kycDocumentGroup struct {
	MasterType string
	Label      string
}

var kycDocumentGroups = []kycDocumentGroup{
	{MasterType: "Adharcard", Label: "Aadhaar Card"},
	{MasterType: "Pancard", Label: "PAN Card"},
	{MasterType: "Bank", Label: "Cancelled Cheque or Passbook"},
}

// bankDocumentColumn maps the two bank codes onto the astrologer_bank_accounts
// column that has to carry the same file, so the financial documents are
// readable from the bank row the payout screens already load.
var bankDocumentColumn = map[string]string{
	"CHEQUE":   "cancelled_cheque",
	"PASSBOOK": "passbook",
}

// kycDocumentCodeAliases lists every document_type that satisfies a KYC group,
// beyond the active master codes.
//
// master_document_types.code and the astrologer_documents.document_type enum
// drifted apart: the active Adharcard code is AADHAAR_CARD, while the documents
// on file carry AADHAAR_FRONT or AADHAAR_BACK (AADHAAR_BACK's master row is
// is_active = 0, so it is not even in the active set), and the enum carries
// both PAN and PANCARD. The group was therefore never satisfied — an astrologer
// with their Aadhaar, PAN and passbook uploaded still got
// is_kyc_doc_updated false and step 9 never completed.
//
// Matching on the group rather than the exact active code means a document
// already uploaded keeps counting after an admin renames or deactivates a code.
var kycDocumentCodeAliases = map[string][]string{
	"ADHARCARD": {"AADHAAR_CARD", "AADHAAR_FRONT", "AADHAAR_BACK"},
	"PANCARD":   {"PANCARD", "PAN"},
	"BANK":      {"CHEQUE", "PASSBOOK"},
}

// kycGroupCodes is the full set of document_type values that satisfy a group:
// the master codes served for it plus the aliases above.
func kycGroupCodes(masterType string, masterCodes []string) map[string]bool {

	codes := make(map[string]bool, len(masterCodes)+3)

	for _, code := range masterCodes {
		codes[strings.ToUpper(strings.TrimSpace(code))] = true
	}

	for _, code := range kycDocumentCodeAliases[strings.ToUpper(masterType)] {
		codes[code] = true
	}

	return codes
}

// uploadedDocumentLabel is the short label is_uploaded_documents reports for a
// master_type. Only the master types that need a name other than their own are
// listed; anything else falls back to master_document_types.name of the code
// that was uploaded, so a master type added in the database appears without a
// change here.
var uploadedDocumentLabel = map[string]string{
	"ADHARCARD": "Aadhaar Card",
	"PANCARD":   "PAN Card",
	"BANK":      "Bank",
}

// uploadedDocuments returns the labels of the document groups the astrologer
// already has on file, in the order GetMasterDocumentTypes serves them. One
// label per master_type — either side of the Aadhaar is still just "Aadhaar
// Card" — and "Bank" is reported when either astrologer_documents or the
// cancelled_cheque/passbook columns of astrologer_bank_accounts carry a file,
// since those two are written together by syncBankDocument.
func (s *astrologerService) uploadedDocuments(astrologer *models.Astrologer) ([]string, error) {

	masters, err := s.repository.GetMasterDocumentTypes()

	if err != nil {
		return nil, err
	}

	types, err := s.repository.GetUploadedDocumentTypes(astrologer.ID)

	if err != nil {
		return nil, err
	}

	uploaded := make(map[string]bool, len(types))

	for _, t := range types {
		uploaded[strings.ToUpper(strings.TrimSpace(t))] = true
	}

	// A cheque or passbook on the bank row counts as an uploaded bank
	// document even if its astrologer_documents row lost its document_type.
	bank, err := s.repository.GetBankAccountByAstrologerID(astrologer.ID)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	bankUploaded := err == nil &&
		(strings.TrimSpace(bank.CancelledCheque) != "" ||
			strings.TrimSpace(bank.Passbook) != "")

	labels := []string{}
	seen := map[string]bool{}

	addLabel := func(label string) {

		label = strings.TrimSpace(label)

		if label == "" || seen[strings.ToUpper(label)] {
			return
		}

		seen[strings.ToUpper(label)] = true
		labels = append(labels, label)
	}

	for _, master := range masters {

		masterType := strings.ToUpper(strings.TrimSpace(master.Type))
		code := strings.ToUpper(strings.TrimSpace(master.Code))

		// Aliases as well, so a document stored under a code the master no
		// longer serves — AADHAAR_FRONT against an AADHAAR_CARD master row —
		// is still reported as uploaded. Same rule pendingKYCDocuments uses.
		isUploaded := uploaded[code] || (masterType == "BANK" && bankUploaded)

		if !isUploaded {

			for alias := range kycGroupCodes(masterType, nil) {

				if uploaded[alias] {
					isUploaded = true
					break
				}
			}
		}

		if !isUploaded {
			continue
		}

		if label, ok := uploadedDocumentLabel[masterType]; ok {
			addLabel(label)
			continue
		}

		addLabel(master.Name)
	}

	// The bank row can hold a file whose master code was deactivated, so the
	// loop above would have skipped it.
	if bankUploaded {
		addLabel(uploadedDocumentLabel["BANK"])
	}

	return labels, nil
}

// pendingKYCDocuments returns the labels of the KYC groups the astrologer has
// nothing on file for, in the order of kycDocumentGroups.
func (s *astrologerService) pendingKYCDocuments(astrologerID uint) ([]string, error) {

	// The active master codes, grouped by master_type exactly as
	// GetMasterDocumentTypes serves them to the app.
	masters, err := s.repository.GetMasterDocumentTypes()

	if err != nil {
		return nil, err
	}

	codesByMasterType := make(map[string][]string)

	for _, master := range masters {

		masterType := strings.ToUpper(strings.TrimSpace(master.Type))

		codesByMasterType[masterType] = append(
			codesByMasterType[masterType],
			strings.ToUpper(strings.TrimSpace(master.Code)),
		)
	}

	types, err := s.repository.GetUploadedDocumentTypes(astrologerID)

	if err != nil {
		return nil, err
	}

	uploaded := make(map[string]bool, len(types))

	for _, t := range types {
		uploaded[strings.ToUpper(strings.TrimSpace(t))] = true
	}

	pending := []string{}

	for _, group := range kycDocumentGroups {

		activeCodes := codesByMasterType[strings.ToUpper(group.MasterType)]

		// An admin who deactivated every code under a master_type has dropped
		// the requirement; demanding a document nobody can upload would wedge
		// step 9 shut. The requirement is still decided by the *active* codes —
		// only the check below is widened.
		if len(activeCodes) == 0 {
			continue
		}

		codes := kycGroupCodes(group.MasterType, activeCodes)

		satisfied := false

		for code := range codes {

			if uploaded[code] {
				satisfied = true
				break
			}
		}

		if !satisfied {
			pending = append(pending, group.Label)
		}
	}

	return pending, nil
}

// ensureKYCDocumentsComplete refuses a call that would carry an astrologer
// past step 9 while a mandatory document is still missing. The error names what
// is outstanding, in the same words /profile/document's kyc_message uses.
func (s *astrologerService) ensureKYCDocumentsComplete(userID uint) error {

	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {
		return err
	}

	pending, err := s.pendingKYCDocuments(astrologer.ID)

	if err != nil {
		return err
	}

	if len(pending) > 0 {
		return errors.New(kycMessage(pending))
	}

	return nil
}

// kycMessage is the kyc_message the app shows on step 9 — confirmation once
// the mandatory financial set is complete, otherwise what is still missing.
func kycMessage(pending []string) string {

	if len(pending) == 0 {
		return "KYC Document Updated…!"
	}

	return fmt.Sprintf(
		"KYC Pending ( %s ) missing please upload kyc document",
		strings.Join(pending, ", "),
	)
}

// syncBankDocument mirrors an uploaded CHEQUE/PASSBOOK onto the astrologer's
// bank row. The requirement is that the financial documents land in both
// astrologer_documents and astrologer_bank_accounts; step 8 creates the bank
// row, so a missing one means the astrologer jumped the order.
func (s *astrologerService) syncBankDocument(
	astrologer *models.Astrologer,
	code string,
	documentName string,
	filePath string,
) error {

	column, ok := bankDocumentColumn[strings.ToUpper(strings.TrimSpace(code))]

	if !ok {
		return nil
	}

	bank, err := s.repository.GetBankAccountByAstrologerID(astrologer.ID)

	if errors.Is(err, gorm.ErrRecordNotFound) {

		return fmt.Errorf(
			"bank account details are required before uploading %s. Please complete the bank details step first",
			documentName,
		)

	} else if err != nil {

		return err
	}

	// Backfill the owner while we are here; rows written before this carried a
	// bank account with no user_id.
	bank.UserId = astrologer.UserID

	if column == "cancelled_cheque" {
		bank.CancelledCheque = filePath
	} else {
		bank.Passbook = filePath
	}

	return s.repository.UpdateBankAccountRow(bank)
}

func (s *astrologerService) UploadDocument(userID uint, req dto.UploadDocumentRequest, file *multipart.FileHeader) (*dto.UploadDocumentResponse, error) {

	// The ten-step flow closes once the profile goes to an admin.
	if err := s.ensureStepEditable(userID); err != nil {
		return nil, err
	}
	// Validate Request
	if strings.TrimSpace(req.MasterType) == "" {
		return nil, errors.New("master type is required")
	}

	if strings.TrimSpace(req.DocumentType) == "" {
		return nil, errors.New("document type is required")
	}

	// Validate File

	if file == nil {
		return nil, errors.New("document file is required")
	}

	// Validate Extension
	ext := strings.ToLower(filepath.Ext(file.Filename))
	allowed := map[string]bool{
		".jpg":  true,
		".jpeg": true,
		".png":  true,
		".pdf":  true,
	}
	if !allowed[ext] {
		return nil, errors.New("only jpg, jpeg, png and pdf files are allowed")
	}

	// Validate File Size (10 MB)
	if file.Size > 10*1024*1024 {
		return nil, errors.New("document size should not exceed 10 MB")
	}

	// Get Astrologer
	astrologer, err := s.repository.GetByUserID(userID)

	if err != nil {
		return nil, err
	}

	// Validate Master Type + Document Type
	masterDoc, err := s.repository.GetMasterDocumentType(
		req.MasterType,
		req.DocumentType,
	)

	if err != nil {
		return nil, errors.New("invalid master type or document type")
	}

	// Upload Folder Mapping
	var uploadFolder string
	switch strings.ToLower(strings.TrimSpace(req.MasterType)) {
	case "adharcard":
		uploadFolder = "uploads/astrologers/documents/adhaarcard"

	case "pancard":
		uploadFolder = "uploads/astrologers/documents/pancard"

	case "bank":
		uploadFolder = "uploads/astrologers/documents/bank"

	case "education":
		uploadFolder = "uploads/astrologers/documents/education"

	case "professional":
		uploadFolder = "uploads/astrologers/documents/professional"

	case "experience":
		uploadFolder = "uploads/astrologers/documents/experience"

	default:
		uploadFolder = "uploads/astrologers/documents/other"
	}

	// Create Folder if Not Exists
	if _, err := os.Stat(uploadFolder); os.IsNotExist(err) {

		if err := os.MkdirAll(uploadFolder, os.ModePerm); err != nil {
			return nil, err
		}
	}

	// Generate Unique File Name
	fileName := fmt.Sprintf(
		"%s_%d%s",
		strings.ToLower(masterDoc.Code),
		time.Now().Unix(),
		ext,
	)
	filePath := filepath.Join(uploadFolder, fileName)
	// Open Uploaded File
	src, err := file.Open()

	if err != nil {
		return nil, err
	}
	defer src.Close()
	// Create Destination File
	dst, err := os.Create(filePath)
	if err != nil {
		return nil, err
	}
	defer dst.Close()
	// Copy Uploaded File
	if _, err = io.Copy(dst, src); err != nil {
		return nil, err
	}
	// Relative Path
	relativePath := filepath.ToSlash(filePath)
	baseURL := strings.TrimRight(os.Getenv("APP_URL"), "/")
	documentURL := fmt.Sprintf("%s/%s", baseURL, relativePath)
	// Check Existing Document
	existingDocument, err := s.repository.GetDocumentByAstrologerAndType(
		astrologer.ID,
		req.DocumentType,
	)
	// A cheque or passbook has to reach astrologer_bank_accounts as well, and
	// that is refused outright when step 8 was skipped — so it runs before the
	// document row is written, not after, to avoid a half-applied upload.
	if err := s.syncBankDocument(
		astrologer,
		masterDoc.Code,
		masterDoc.Name,
		relativePath,
	); err != nil {
		return nil, err
	}

	// Update Existing Document
	if err == nil {
		existingDocument.UserID = astrologer.UserID
		existingDocument.DocumentType = req.DocumentType
		existingDocument.DocumentNumber = req.DocumentNumber
		existingDocument.DocumentName = fileName
		existingDocument.FilePath = relativePath
		existingDocument.FileSize = file.Size
		existingDocument.MimeType = file.Header.Get("Content-Type")
		existingDocument.VerificationStatus = "pending"
		existingDocument.IsActive = true

		if err := s.repository.UpdateDocument(existingDocument); err != nil {
			return nil, err
		}

		// Update Profile Progress
		//
		// The KYC check runs first, because step 9 counts as finished only once
		// every mandatory document is on file. Raising current_step on the
		// first upload made /profile/progress report step 9 completed while
		// this endpoint was still answering is_kyc_doc_updated false.
		pending, err := s.pendingKYCDocuments(astrologer.ID)

		if err != nil {
			return nil, err
		}

		if len(pending) == 0 {
			s.updateProgress(astrologer, constants.StepDocuments)
		}

		s.calculateCompletion(astrologer)
		s.updateProfileStatus(astrologer)

		if err := s.repository.Update(astrologer); err != nil {
			return nil, err
		}

		response := &dto.UploadDocumentResponse{
			DocumentID:           existingDocument.ID,
			MasterType:           masterDoc.Type,
			DocumentType:         existingDocument.DocumentType,
			DocumentNumber:       existingDocument.DocumentNumber,
			DocumentName:         existingDocument.DocumentName,
			DocumentPath:         existingDocument.FilePath,
			DocumentURL:          documentURL,
			FileSize:             existingDocument.FileSize,
			MimeType:             existingDocument.MimeType,
			VerificationStatus:   existingDocument.VerificationStatus,
			CurrentStep:          constants.StepDocuments,
			CompletionPercentage: astrologer.CompletionPercentage,
		}

		uploadedDocs, err := s.uploadedDocuments(astrologer)

		if err != nil {
			return nil, err
		}

		applyKYCNextStep(response, pending, uploadedDocs)

		return response, nil
	}

	// Create New Document
	document := &models.AstrologerDocument{

		UserID: astrologer.UserID,

		AstrologerID: astrologer.ID,

		DocumentType: req.DocumentType,

		DocumentNumber: req.DocumentNumber,

		DocumentName: fileName,

		FilePath: relativePath,

		FileSize: file.Size,

		MimeType: file.Header.Get("Content-Type"),

		VerificationStatus: "pending",

		IsActive: true,
	}

	// Save Document
	if err := s.repository.SaveDocument(document); err != nil {
		return nil, err
	}
	// Update Profile Progress — step 9 only counts once the mandatory KYC set
	// is complete, same as the update path above.
	pending, err := s.pendingKYCDocuments(astrologer.ID)

	if err != nil {
		return nil, err
	}

	if len(pending) == 0 {
		s.updateProgress(astrologer, constants.StepDocuments)
	}

	s.calculateCompletion(astrologer)
	s.updateProfileStatus(astrologer)
	if err := s.repository.Update(astrologer); err != nil {
		return nil, err
	}

	// Create Progress Record
	progress := &models.AstrologerProfileProgress{

		UserID: userID,

		AstrologerID: astrologer.ID,

		CurrentStep: astrologer.CurrentStep,

		CompletionPercentage: astrologer.CompletionPercentage,

		ProfileCompleted: false,

		IsCompleted: false,

		Submitted: false,

		AdminStatus: astrologer.ProfileStatus,
	}

	if err := s.repository.UpdateProfileProgress(progress); err != nil {
		return nil, err
	}

	//----------------------------------------
	// Create Verification Log
	//----------------------------------------

	oldStatus := constants.ProfileInProgress

	if astrologer.ProfileStatus == constants.ProfileSubmitted {
		oldStatus = constants.ProfileSubmitted
	}

	if err := s.createVerificationLog(config.DB, astrologer.ID, oldStatus, astrologer.ProfileStatus); err != nil {
		return nil, err
	}

	//----------------------------------------
	// Response
	//----------------------------------------

	response := &dto.UploadDocumentResponse{

		DocumentID:         document.ID,
		MasterType:         masterDoc.Type,
		DocumentType:       document.DocumentType,
		DocumentNumber:     document.DocumentNumber,
		DocumentName:       document.DocumentName,
		DocumentPath:       document.FilePath,
		DocumentURL:        documentURL,
		FileSize:           document.FileSize,
		MimeType:           document.MimeType,
		VerificationStatus: document.VerificationStatus,

		CurrentStep:          constants.StepDocuments,
		CompletionPercentage: astrologer.CompletionPercentage,
	}

	uploadedDocs, err := s.uploadedDocuments(astrologer)

	if err != nil {
		return nil, err
	}

	applyKYCNextStep(response, pending, uploadedDocs)

	return response, nil
}

// applyKYCNextStep decides where step 9 sends the astrologer next. The upload
// itself succeeded either way; the flow only moves on to Submit Profile once
// every mandatory financial document is on file, and otherwise stays on the
// document screen carrying the list of what is still missing.
func applyKYCNextStep(response *dto.UploadDocumentResponse, pending []string, uploaded []string) {

	response.PendingDocuments = pending
	response.IsUploadedDocuments = uploaded
	response.IsKYCDocUpdated = len(pending) == 0
	response.KYCMessage = kycMessage(pending)

	if response.IsKYCDocUpdated {

		response.NextStep = constants.StepAvailability
		response.NextScreen = "Submit Profile"

		return
	}

	response.NextStep = constants.StepDocuments
	response.NextScreen = "Upload Documents"
}

// GetProfileProgress reports how far the profile got and which step is next.
//
// A verified astrologer who has not saved step 1 has no astrologer row yet, and
// that is exactly where the app calls this, so a missing row answers "step 1
// pending" instead of failing with a record-not-found.
func (s *astrologerService) GetProfileProgress(userID uint) (*dto_astrologer.ProfileProgressResponse, error) {

	profile, err := s.repository.GetProfileProgress(userID)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	response := &dto_astrologer.ProfileProgressResponse{
		ProfileStatus: constants.ProfileIncomplete,
	}

	if profile != nil {

		response.CompletionPercentage = profile.CompletionPercentage
		response.CurrentStep = profile.CurrentStep
		response.ProfileStatus = profile.ProfileStatus
		response.IsProfileCompleted = profile.IsProfileCompleted
		response.AdminStatus = profile.AdminStatus
	}

	pendingStep := PendingProfileStep(profile)

	response.PendingStep = pendingStep
	response.PendingStepTitle = ProfileStepTitle(pendingStep)
	response.NextScreen = ProfileScreen(profile)
	response.CanSubmit = AllProfileStepsDone(profile)

	// Step 9 answers from the documents themselves rather than from
	// current_step, so it always agrees with is_kyc_doc_updated on
	// /profile/document. current_step alone was wrong in both directions: it
	// reported the step finished after a single upload, and a row already at
	// step 10 from an earlier build reports it finished with documents still
	// missing.
	kycComplete := false

	if profile != nil {

		pendingDocuments, err := s.pendingKYCDocuments(profile.ID)

		if err != nil {
			return nil, err
		}

		kycComplete = len(pendingDocuments) == 0
	}

	// A profile with KYC outstanding belongs on the document screen and cannot
	// submit, whatever current_step says.
	if !kycComplete && response.PendingStep > constants.StepDocuments {

		response.PendingStep = constants.StepDocuments
		response.PendingStepTitle = ProfileStepTitle(constants.StepDocuments)
		response.CanSubmit = false
	}

	for step := constants.StepBasicProfile; step <= constants.StepAvailability; step++ {

		completed := response.CurrentStep >= step

		if step == constants.StepDocuments {
			completed = kycComplete
		}

		response.Steps = append(response.Steps,
			dto_astrologer.ProfileStepResponse{
				CurrentStep: step,
				Title:       ProfileStepTitle(step),
				Completed:   completed,
			},
		)
	}

	return response, nil
}
func (s *astrologerService) ChangeStatus(userID uint, req dto_astrologer.ChangeStatusRequest) error {

	return s.repository.ChangeStatus(userID, req.Status)
}

func (s *astrologerService) DeleteAccount(userID uint) error {

	return s.repository.DeleteAccount(userID)
}

func (s *astrologerService) GetMasterLanguages(userID uint) ([]dto_astrologer.LanguageMasterResponse, error) {

	return s.repository.GetMasterLanguages(userID)
}

func (s *astrologerService) GetMasterSkills(userID uint) ([]dto_astrologer.SkillMasterResponse, error) {

	return s.repository.GetMasterSkills(userID)
}

func (s *astrologerService) GetMasterDocumentTypes() ([]dto_astrologer.MasterDocumentResponse, error) {

	documents, err := s.repository.GetMasterDocumentTypes()

	if err != nil {
		return nil, err
	}

	groupMap := make(map[string][]dto_astrologer.DocumentTypeResponse)

	for _, doc := range documents {

		groupMap[doc.Type] = append(groupMap[doc.Type],
			dto_astrologer.DocumentTypeResponse{
				ID:         doc.ID,
				Name:       doc.Name,
				Code:       doc.Code,
				IsRequired: doc.IsRequired,
			})
	}

	var response []dto_astrologer.MasterDocumentResponse

	for masterType, list := range groupMap {

		response = append(response,
			dto_astrologer.MasterDocumentResponse{
				MasterType:   masterType,
				DocumentList: list,
			})
	}

	sort.Slice(response, func(i, j int) bool {
		return response[i].MasterType < response[j].MasterType
	})

	return response, nil
}

func (s *astrologerService) GetDeviceTypes() ([]dto_astrologer.DeviceTypeResponse, error) {

	data, err := s.repository.GetDeviceTypes()

	if err != nil {
		return nil, err
	}

	var response []dto_astrologer.DeviceTypeResponse

	for _, v := range data {

		response = append(response, dto_astrologer.DeviceTypeResponse{

			ID: v.ID,

			Name: v.Name,

			Code: v.Code,
		})
	}

	return response, nil
}

func (s *astrologerService) GetWorkingHours() ([]dto_astrologer.WorkingHourResponse, error) {

	data, err := s.repository.GetWorkingHours()

	if err != nil {
		return nil, err
	}

	var response []dto_astrologer.WorkingHourResponse

	for _, v := range data {

		response = append(response, dto_astrologer.WorkingHourResponse{

			ID: v.ID,

			Hours: v.Hours,

			Title: v.Title,
		})
	}

	return response, nil
}

func (s *astrologerService) GetAvailability(
	userID uint,
) (*dto_astrologer.GetAvailabilityResponse, error) {

	astrologer, err := s.repository.GetByUserID(userID)
	if err != nil {
		return nil, err
	}

	schedules, err := s.repository.GetAvailableScheduleByAstrologerID(astrologer.ID)
	if err != nil {
		return nil, err
	}

	social, _ := s.repository.GetSocialLinksByAstrologerID(astrologer.ID)

	resp := &dto_astrologer.GetAvailabilityResponse{

		DeviceType: astrologer.DeviceType,

		DailyWorkingHours: astrologer.DailyHours,

		IsWorkingOnPlatform: astrologer.WorkingPlatform,

		PlatformName: astrologer.WorkingPlatformName,

		ChatRate: astrologer.ChatRate,

		CallRate: astrologer.Charge,

		FreeChat: astrologer.FreeChat,

		FreeCall: astrologer.FreeCall,
	}

	if len(schedules) > 0 {

		resp.InterviewFrom = schedules[0].InterviewFrom

		resp.InterviewTo = schedules[0].InterviewTo

		resp.Timezone = schedules[0].Timezone
	}

	for _, v := range schedules {

		resp.Schedule = append(resp.Schedule,
			dto_astrologer.AvailabilityItem{

				Day: v.Day,

				FromTime: v.FromTime,

				ToTime: v.ToTime,

				SlotDuration: v.SlotDuration,

				IsBreak: v.IsBreak,
			})
	}

	if social != nil {

		resp.SocialLinks = dto_astrologer.SocialLinksResponse{

			Facebook: social.Facebook,

			Instagram: social.Instagram,

			Youtube: social.Youtube,

			Telegram: social.Telegram,

			Linkedin: social.Linkedin,

			Website: social.Website,
		}
	}

	return resp, nil
}

func (s *astrologerService) validateProfileCompletion(
	completion *models.AstrologerProfileCompletion,
) error {

	if !completion.BasicProfile {
		return errors.New("please complete Basic Information")
	}

	if !completion.Languages {
		return errors.New("please complete Languages")
	}

	if !completion.Skills {
		return errors.New("please complete Skills")
	}

	if !completion.ProfileImage {
		return errors.New("please upload Profile Image")
	}

	if !completion.Education {
		return errors.New("please complete Education")
	}

	if !completion.Professional {
		return errors.New("please complete Professional Details")
	}

	if !completion.Experience {
		return errors.New("please complete Experience Details")
	}

	if !completion.Bank {
		return errors.New("please complete Bank Details")
	}

	if !completion.Documents {
		return errors.New("please upload Documents")
	}

	if !completion.Availability {
		return errors.New("please complete Availability")
	}

	return nil
}

func (s *astrologerService) calculateCompletionPercentage(
	completion *models.AstrologerProfileCompletion,
) int {

	total := 10
	completed := 0

	if completion.BasicProfile {
		completed++
	}

	if completion.Languages {
		completed++
	}

	if completion.Skills {
		completed++
	}

	if completion.ProfileImage {
		completed++
	}

	if completion.Education {
		completed++
	}

	if completion.Professional {
		completed++
	}

	if completion.Experience {
		completed++
	}

	if completion.Bank {
		completed++
	}

	if completion.Documents {
		completed++
	}

	if completion.Availability {
		completed++
	}

	return (completed * 100) / total
}

func (s *astrologerService) updateCompletionStep(
	completion *models.AstrologerProfileCompletion,
	step int,
) {

	switch step {

	case constants.StepBasicProfile:
		completion.BasicProfile = true

	case constants.StepLanguages:
		completion.Languages = true

	case constants.StepSkills:
		completion.Skills = true

	case constants.StepProfileImage:
		completion.ProfileImage = true

	case constants.StepEducation:
		completion.Education = true

	case constants.StepProfessional:
		completion.Professional = true

	case constants.StepExperience:
		completion.Experience = true

	case constants.StepBank:
		completion.Bank = true

	case constants.StepDocuments:
		completion.Documents = true

	case constants.StepAvailability:
		completion.Availability = true
	}

	completion.CompletionPercentage =
		s.calculateCompletionPercentage(completion)
}

func (s *astrologerService) createOrUpdateProfileCompletion(
	astrologerID uint,
	step int,
) error {

	completion, err :=
		s.repository.GetProfileCompletionByAstrologerID(astrologerID)

	if err != nil {

		completion = &models.AstrologerProfileCompletion{
			AstrologerID: astrologerID,
		}

		s.updateCompletionStep(completion, step)

		return s.repository.CreateProfileCompletion(completion)
	}

	s.updateCompletionStep(completion, step)

	return s.repository.UpdateProfileCompletion(completion)
}

func (s *astrologerService) createInterviewRound1(userID uint) error {

	// Already Exists?
	_, err := s.repository.GetInterviewByUserID(userID)
	if err == nil {
		return nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	// Create Interview
	interview := &models.AstrologerInterview{
		UserID:      userID,
		RoundNo:     1,
		Result:      constants.InterviewPending,
		Remarks:     "",
		ScheduledAt: nil,
		Interviewer: 0,
	}
	return s.repository.CreateInterview(interview)
}

func (s *astrologerService) createProfileCompletion(astrologerID uint) error {

	progress, err := s.repository.GetProfileProgressByAstrologerID(astrologerID)
	if err != nil {
		return err
	}

	completion, err := s.repository.GetProfileCompletionByAstrologerID(astrologerID)

	if err != nil {

		completion = &models.AstrologerProfileCompletion{
			AstrologerID:         astrologerID,
			BasicProfile:         true,
			Languages:            true,
			ProfileImage:         true,
			Skills:               true,
			Education:            true,
			Professional:         true,
			Experience:           true,
			Bank:                 true,
			Documents:            true,
			SocialLinks:          true,
			Availability:         true,
			CompletionPercentage: progress.CompletionPercentage,
		}

		return s.repository.CreateProfileCompletion(completion)
	}

	completion.BasicProfile = true
	completion.Languages = true
	completion.ProfileImage = true
	completion.Skills = true
	completion.Education = true
	completion.Professional = true
	completion.Experience = true
	completion.Bank = true
	completion.Documents = true
	completion.SocialLinks = true
	completion.Availability = true
	completion.CompletionPercentage = progress.CompletionPercentage

	return s.repository.UpdateProfileCompletion(completion)
}

func (s *astrologerService) updateProgressTable(
	userID uint,
	astrologerID uint,
	currentStep int,
	completionPercentage int,
) error {

	progress, err := s.repository.GetProgressByAstrologerID(astrologerID)

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {

			submittedAt := time.Now()
			progress = &models.AstrologerProgress{
				UserID:               userID,
				AstrologerID:         astrologerID,
				CurrentStep:          currentStep,
				CompletionPercentage: completionPercentage,
				ProfileCompleted:     completionPercentage >= 100,
				Submitted:            false,
				SubmittedAt:          &submittedAt,
				AdminStatus:          constants.AdminPending,
				Iscompleted:          true,
				ApprovedAt:           nil,
				RejectedAt:           nil,
				Remarks:              "",
			}

			return s.repository.CreateProgress(progress)
		}

		return err
	}

	progress.CurrentStep = currentStep
	progress.CompletionPercentage = completionPercentage
	progress.ProfileCompleted = completionPercentage >= 100

	return s.repository.UpdateProgress(progress)
}

func (s *astrologerService) UpdateProfile(userID uint, req dto.UpdateProfileRequest) (*dto.UpdateProfileResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	var response *dto.UpdateProfileResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		user, err := s.repository.GetUserTableByID(userID)

		if err != nil {
			return err
		}

		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}

		// if err := s.validateProfileEditable(astrologer); err != nil {
		// 	return err
		// }

		user.Name = req.Name
		user.Gender = req.Gender
		user.Location = req.City
		user.State = req.State
		user.Country = req.Country
		user.AddressLine1 = req.Address
		user.Pincode = req.Pincode

		if req.DateOfBirth != "" {

			dob, err := time.Parse("2006-01-02", req.DateOfBirth)

			if err != nil {
				return errors.New("invalid date format")
			}

			user.BirthDate = dob.Format("2006-01-02")

			astrologer.BirthDate = &dob
		}

		astrologer.Name = req.Name
		astrologer.Gender = req.Gender
		astrologer.City = req.City
		astrologer.State = req.State
		astrologer.Country = req.Country
		astrologer.Bio = req.Bio
		astrologer.ExperienceYears = req.ExperienceYears
		astrologer.DailyHours = req.DailyHours
		astrologer.WorkingPlatform = req.WorkingPlatform
		astrologer.WorkingPlatformName = req.WorkingPlatformName
		astrologer.LearnAstrology = req.LearnAstrology

		err = s.repository.UpdateBasicProfile(
			tx,
			user,
			astrologer,
		)

		if err != nil {
			return err
		}

		response = &dto.UpdateProfileResponse{

			ID: astrologer.ID,

			Name: astrologer.Name,

			Email: astrologer.Email,

			Mobile: astrologer.ContactNo,

			ProfileImage: astrologer.ProfileImage,

			CurrentStep: astrologer.CurrentStep,

			CompletionPercentage: astrologer.CompletionPercentage,

			Message: "Profile updated successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) UpdateProfileImage(userID uint, file *multipart.FileHeader) (*dto.UpdateProfileImageResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	var response *dto.UpdateProfileImageResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		if file == nil {
			return errors.New("profile image is required")
		}

		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Delete old image
		if astrologer.ProfileImage != "" {

			if _, err := os.Stat(astrologer.ProfileImage); err == nil {
				_ = os.Remove(astrologer.ProfileImage)
			}
		}

		ext := strings.ToLower(filepath.Ext(file.Filename))

		allowed := map[string]bool{
			".jpg":  true,
			".jpeg": true,
			".png":  true,
			".webp": true,
		}

		if !allowed[ext] {
			return errors.New("only jpg, jpeg, png and webp allowed")
		}

		if file.Size > 5*1024*1024 {
			return errors.New("image size should be less than 5MB")
		}

		uploadDir := "uploads/astrologers/profile"

		if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
			if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
				return err
			}
		}

		fileName := fmt.Sprintf(
			"astro_%d_%d%s",
			userID,
			time.Now().Unix(),
			ext,
		)

		dst := filepath.Join(uploadDir, fileName)

		src, err := file.Open()
		if err != nil {
			return err
		}
		defer src.Close()

		out, err := os.Create(dst)
		if err != nil {
			return err
		}
		defer out.Close()

		if _, err = io.Copy(out, src); err != nil {
			return err
		}

		imagePath := filepath.ToSlash(filepath.Join(
			"uploads",
			"astrologers",
			"profile",
			fileName,
		))

		if err := s.repository.UpdateProfileImage(
			tx,
			astrologer.ID,
			imagePath,
		); err != nil {
			return err
		}

		baseURL := strings.TrimRight(os.Getenv("APP_URL"), "/")

		response = &dto.UpdateProfileImageResponse{
			ProfileImage: fmt.Sprintf("%s/%s", baseURL, imagePath),
			Message:      "Profile image updated successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) DeleteProfileImage(userID uint) (*dto.DeleteProfileImageResponse, error) {

	var response *dto.DeleteProfileImageResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}

		if astrologer.ProfileImage == "" {
			return errors.New("profile image not found")
		}

		if _, err := os.Stat(astrologer.ProfileImage); err == nil {
			_ = os.Remove(astrologer.ProfileImage)
		}

		err = s.repository.DeleteProfileImage(
			tx,
			userID,
			astrologer.ID,
		)

		if err != nil {
			return err
		}

		response = &dto.DeleteProfileImageResponse{
			Message: "Profile image deleted successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) UpdateAvailability(userID uint, req dto_astrologer.SaveAvailabilityRequest) (*dto_astrologer.UpdateAvailabilityResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	var response *dto_astrologer.UpdateAvailabilityResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		// Get astrologer
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Validation
		if req.DailyWorkingHours <= 0 {
			return errors.New("daily working hours must be greater than zero")
		}

		if req.ChatRate < 0 {
			return errors.New("invalid chat rate")
		}

		if req.CallRate < 0 {
			return errors.New("invalid call rate")
		}

		// Update astrologer information
		astrologer.DeviceType = req.DeviceType
		astrologer.DailyHours = req.DailyWorkingHours
		astrologer.ChatRate = req.ChatRate
		astrologer.AudioCallRate = req.CallRate
		astrologer.VideoCallRate = req.VideoCallRate
		astrologer.Charge = req.CallRate
		astrologer.WorkingPlatform = req.IsWorkingOnPlatform
		astrologer.WorkingPlatformName = req.PlatformName
		astrologer.FreeChat = req.FreeChat
		astrologer.FreeCall = req.FreeCall
		astrologer.ChatStatus = "Completed"
		astrologer.ChatWaitTime = "Completed"
		astrologer.CallStatus = "Completed"
		astrologer.CallWaitTime = "Completed"

		// The education fields are not part of a professional update, so they
		// simply keep whatever the row was loaded with. They used to be assigned
		// to themselves here, which did the same thing and read as an oversight.

		astrologer.InstaProfileLink = req.SocialLinks.Instagram
		astrologer.FacebookProfileLink = req.SocialLinks.Facebook
		astrologer.YoutubeProfileLink = req.SocialLinks.Youtube
		astrologer.TelegramProfileLink = req.SocialLinks.Telegram
		astrologer.LinkedinProfileLink = req.SocialLinks.Linkedin
		astrologer.WebsiteProfileLink = req.SocialLinks.Website
		astrologer.Nameofplateform = req.PlatformName

		if err := s.repository.UpdateAstrologer(tx, astrologer); err != nil {
			return err
		}

		// Delete previous availability
		if err := s.repository.DeleteAvailabilityByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		// Insert new schedule
		for _, item := range req.Schedule {

			availability := models.AstrologerAvailability{
				AstrologerID: astrologer.ID,
				Day:          item.Day,
				FromTime:     item.FromTime,
				ToTime:       item.ToTime,
				SlotDuration: item.SlotDuration,
				IsBreak:      item.IsBreak,
			}

			if err := s.repository.CreateAvailability(
				tx,
				&availability,
			); err != nil {
				return err
			}
		}

		// Save Social Links
		socialLink, err := s.repository.GetSocialLinksByAstrologerID(
			astrologer.ID,
		)

		if err != nil {

			newSocial := models.AstrologerSocialLink{
				AstrologerID: astrologer.ID,
				Facebook:     req.SocialLinks.Facebook,
				Instagram:    req.SocialLinks.Instagram,
				Youtube:      req.SocialLinks.Youtube,
				Telegram:     req.SocialLinks.Telegram,
				Linkedin:     req.SocialLinks.Linkedin,
				Website:      req.SocialLinks.Website,
			}

			if err := s.repository.CreateSocialLinks(
				tx,
				&newSocial,
			); err != nil {
				return err
			}

		} else {

			socialLink.Facebook = req.SocialLinks.Facebook
			socialLink.Instagram = req.SocialLinks.Instagram
			socialLink.Youtube = req.SocialLinks.Youtube
			socialLink.Telegram = req.SocialLinks.Telegram
			socialLink.Linkedin = req.SocialLinks.Linkedin
			socialLink.Website = req.SocialLinks.Website

			if err := s.repository.UpdateSocialLinks(
				tx,
				socialLink,
			); err != nil {
				return err
			}
		}

		response = &dto_astrologer.UpdateAvailabilityResponse{
			Message: "Availability updated successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) UpdateBankAccount(userID uint, req dto_astrologer.UpdateBankAccountRequest) (*dto_astrologer.UpdateBankAccountResponse, error) {
	return nil, errors.New("not implemented")
}

func (s *astrologerService) UpdateSkills(
	userID uint,
	req dto_astrologer.UpdateSkillsRequest,
) (*dto_astrologer.UpdateSkillsResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	var response *dto_astrologer.UpdateSkillsResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		if len(req.SkillIDs) == 0 {
			return errors.New("please select at least one skill")
		}

		// Remove old skills
		if err := s.repository.DeleteSkillsByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		// Insert new skills
		for _, skillID := range req.SkillIDs {

			// Optional: Validate skill exists
			if _, err := s.repository.GetSkillByID(skillID); err != nil {
				return fmt.Errorf("invalid skill id %d", skillID)
			}

			skill := &models.AstrologerSkill{
				AstrologerID: astrologer.ID,
				SkillID:      skillID,
			}

			// On the transaction, so the insert does not deadlock against the
			// DELETE above and is rolled back with it if anything fails.
			if err := s.repository.CreateSkillTx(tx, skill); err != nil {
				return err
			}
		}

		response = &dto_astrologer.UpdateSkillsResponse{
			Message: "Skills updated successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) UpdateLanguages(
	userID uint,
	req dto_astrologer.UpdateLanguagesRequest,
) (*dto_astrologer.UpdateLanguagesResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	var response *dto_astrologer.UpdateLanguagesResponse

	err := s.executeTransaction(func(tx *gorm.DB) error {

		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Validated (and de-duplicated) against the master up front, so the
		// delete below cannot wipe the selection on a payload that will fail.
		selectedIDs, err := s.validateLanguageIDs(req.LanguageIDs)

		if err != nil {
			return err
		}

		if err := s.repository.DeleteLanguagesByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		for _, languageID := range selectedIDs {

			language := &models.AstrologerLanguage{
				AstrologerID: astrologer.ID,
				LanguageID:   languageID,
			}

			// On the transaction, so the insert does not deadlock against the
			// DELETE above and is rolled back with it if anything fails.
			if err := s.repository.CreateLanguageTx(
				tx,
				language,
			); err != nil {
				return err
			}
		}

		response = &dto_astrologer.UpdateLanguagesResponse{
			Message: "Languages updated successfully.",
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return response, nil
}

func (s *astrologerService) UpdateEducation(
	userID uint,
	req dto.UpdateEducationRequest,
) (*dto.UpdateEducationResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	resp := &dto.UpdateEducationResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}

		// Same rule as SaveEducation: this endpoint replaces the whole set, so
		// a payload without UG would delete the UG row rather than leave it.
		if err := validateEducationRequest(req.UG, req.PG); err != nil {
			return err
		}

		// Delete old education
		if err := s.repository.DeleteEducations(astrologer.ID); err != nil {
			return err
		}

		// Save UG
		{
			education := &models.AstrologerEducation{
				AstrologerID:   astrologer.ID,
				EducationType:  "UG",
				Degree:         strings.TrimSpace(req.UG.Degree),
				Institute:      strings.TrimSpace(req.UG.Institute),
				Specialization: strings.TrimSpace(req.UG.FieldOfStudy),
				PassingYear:    req.UG.PassingYear,
			}

			if err := s.repository.CreateEducation(education); err != nil {
				return err
			}
		}

		// Save PG
		if educationBlockFilled(req.PG) {

			education := &models.AstrologerEducation{
				AstrologerID:   astrologer.ID,
				EducationType:  "PG",
				Degree:         strings.TrimSpace(req.PG.Degree),
				Institute:      strings.TrimSpace(req.PG.Institute),
				Specialization: strings.TrimSpace(req.PG.FieldOfStudy),
				PassingYear:    req.PG.PassingYear,
			}

			if err := s.repository.CreateEducation(education); err != nil {
				return err
			}
		}

		// DON'T update onboarding progress here
		// Don't call:
		// updateProgress()
		// calculateCompletion()
		// updateProfileStatus()
		// UpdateProfileProgress()

		resp.Message = "Education updated successfully."

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) UpdateProfessional(
	userID uint,
	req dto.UpdateProfessionalRequest,
) (*dto.UpdateProfessionalResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	resp := &dto.UpdateProfessionalResponse{}

	err := s.executeTransaction(func(tx *gorm.DB) error {

		//----------------------------------
		// Get Astrologer
		//----------------------------------

		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}

		//----------------------------------
		// Validation
		//----------------------------------

		if len(req.Professionals) == 0 {
			return errors.New("professional details are required")
		}

		//----------------------------------
		// Delete Existing Records
		//----------------------------------

		if err := s.repository.DeleteProfessionalByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}

		//----------------------------------
		// Save Professional Records
		//----------------------------------

		for _, p := range req.Professionals {

			entry := professionalEntry{
				Company:          p.CurrentCompany,
				Designation:      p.Occupation,
				ExperienceYears:  p.ExperienceYears,
				StartYear:        p.StartYear,
				EndYear:          p.EndYear,
				CurrentlyWorking: p.CurrentlyWorking,
			}

			if err := validateProfessionalEntry(entry); err != nil {
				return err
			}

			pro := models.AstrologerProfessional{

				AstrologerID: astrologer.ID,

				CurrentCompany: strings.TrimSpace(p.CurrentCompany),

				Occupation: strings.TrimSpace(p.Occupation),

				ExperienceYears: p.ExperienceYears,

				StartYear: p.StartYear,

				EndYear: p.EndYear,

				CurrentlyWorking: *p.CurrentlyWorking,
			}

			if err := s.repository.CreateProfessional(
				tx,
				&pro,
			); err != nil {
				return err
			}
		}

		//----------------------------------
		// No Profile Progress Update
		//----------------------------------

		resp.Message = "Professional details updated successfully."

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) UpdateExperience(userID uint, req dto.UpdateExperienceRequest) (*dto.UpdateExperienceResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}

	resp := &dto.UpdateExperienceResponse{}
	err := s.executeTransaction(func(tx *gorm.DB) error {
		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)

		if err != nil {
			return err
		}
		// Validation
		if len(req.Experiences) == 0 {
			return errors.New("experience details are required")
		}
		// Delete Existing Experience
		if err := s.repository.DeleteExperienceByAstrologerID(
			tx,
			astrologer.ID,
		); err != nil {
			return err
		}
		// Save Experience
		for _, item := range req.Experiences {
			if strings.TrimSpace(item.Title) == "" {
				return errors.New("title is required")
			}
			if item.ExperienceYears < 0 {
				return errors.New("experience years cannot be negative")
			}
			if err := validateExperienceYears(
				item.StartYear,
				item.EndYear,
			); err != nil {
				return err
			}
			exp := models.AstrologerExperience{
				AstrologerID:    astrologer.ID,
				Title:           strings.TrimSpace(item.Title),
				ExperienceYears: item.ExperienceYears,
				StartYear:       item.StartYear,
				EndYear:         item.EndYear,
				IsCurrent:       item.Working(),
				Description:     item.Description,
			}
			if err := s.repository.CreateExperience(
				tx,
				&exp,
			); err != nil {

				return err
			}
		}

		// Don't Update Profile Progress
		resp.Message = "Experience details updated successfully."

		return nil
	})

	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (s *astrologerService) UpdateDocument(userID uint, req dto.UploadDocumentRequest, file *multipart.FileHeader) (*dto.UpdateDocumentResponse, error) {

	// Open to an approved astrologer, closed while an admin holds the profile.
	if err := s.ensureUpdateAllowed(userID); err != nil {
		return nil, err
	}
	var response *dto.UpdateDocumentResponse
	err := s.executeTransaction(func(tx *gorm.DB) error {
		// Validation
		if strings.TrimSpace(req.MasterType) == "" {
			return errors.New("master type is required")
		}

		if strings.TrimSpace(req.DocumentType) == "" {
			return errors.New("document type is required")
		}

		if file == nil {
			return errors.New("document file is required")
		}

		ext := strings.ToLower(filepath.Ext(file.Filename))

		allowed := map[string]bool{
			".jpg":  true,
			".jpeg": true,
			".png":  true,
			".pdf":  true,
		}
		if !allowed[ext] {
			return errors.New("only jpg, jpeg, png and pdf allowed")
		}
		if file.Size > 10*1024*1024 {
			return errors.New("document size should not exceed 10 MB")
		}
		// Get Astrologer
		astrologer, err := s.repository.GetByUserID(userID)
		if err != nil {
			return err
		}
		// Validate Master Type
		masterDoc, err := s.repository.GetMasterDocumentType(
			req.MasterType,
			req.DocumentType,
		)

		if err != nil {
			return errors.New("invalid master type or document type")
		}

		// Folder
		uploadFolder := "uploads/astrologers/documents/other"

		switch strings.ToLower(req.MasterType) {

		case "adharcard":
			uploadFolder = "uploads/astrologers/documents/adhaarcard"

		case "pancard":
			uploadFolder = "uploads/astrologers/documents/pancard"

		case "bank":
			uploadFolder = "uploads/astrologers/documents/bank"

		case "education":
			uploadFolder = "uploads/astrologers/documents/education"

		case "professional":
			uploadFolder = "uploads/astrologers/documents/professional"

		case "experience":
			uploadFolder = "uploads/astrologers/documents/experience"
		}

		if _, err := os.Stat(uploadFolder); os.IsNotExist(err) {

			if err := os.MkdirAll(uploadFolder, os.ModePerm); err != nil {
				return err
			}
		}

		// Save File
		fileName := fmt.Sprintf(
			"%s_%d%s",
			strings.ToLower(masterDoc.Code),
			time.Now().Unix(),
			ext,
		)

		filePath := filepath.Join(uploadFolder, fileName)

		src, err := file.Open()

		if err != nil {
			return err
		}

		defer src.Close()

		dst, err := os.Create(filePath)

		if err != nil {
			return err
		}

		defer dst.Close()

		if _, err = io.Copy(dst, src); err != nil {
			return err
		}

		relativePath := filepath.ToSlash(filePath)

		baseURL := strings.TrimRight(os.Getenv("APP_URL"), "/")

		documentURL := fmt.Sprintf(
			"%s/%s",
			baseURL,
			relativePath,
		)

		// Existing Document
		document, err := s.repository.GetDocumentByAstrologerAndType(
			astrologer.ID,
			req.DocumentType,
		)

		// A replaced cheque/passbook has to reach the bank row too.
		if err := s.syncBankDocument(
			astrologer,
			masterDoc.Code,
			masterDoc.Name,
			relativePath,
		); err != nil {
			return err
		}

		if err == nil {

			document.UserID = astrologer.UserID
			document.DocumentNumber = req.DocumentNumber
			document.DocumentName = fileName
			document.FilePath = relativePath
			document.FileSize = file.Size
			document.MimeType = file.Header.Get("Content-Type")
			document.VerificationStatus = "pending"

			if err := s.repository.UpdateDocument(document); err != nil {
				return err
			}

			response = &dto.UpdateDocumentResponse{

				DocumentID: document.ID,

				MasterType: masterDoc.Type,

				DocumentType: document.DocumentType,

				DocumentNumber: document.DocumentNumber,

				DocumentName: document.DocumentName,

				DocumentPath: document.FilePath,

				DocumentURL: documentURL,

				FileSize: document.FileSize,

				MimeType: document.MimeType,

				VerificationStatus: document.VerificationStatus,

				Message: "Document updated successfully.",
			}

			return nil
		}

		// Create New Document
		newDoc := &models.AstrologerDocument{
			UserID:             astrologer.UserID,
			AstrologerID:       astrologer.ID,
			DocumentType:       req.DocumentType,
			DocumentNumber:     req.DocumentNumber,
			DocumentName:       fileName,
			FilePath:           relativePath,
			FileSize:           file.Size,
			MimeType:           file.Header.Get("Content-Type"),
			VerificationStatus: "pending",
			IsActive:           true,
		}
		if err := s.repository.SaveDocument(newDoc); err != nil {
			return err
		}
		response = &dto.UpdateDocumentResponse{
			DocumentID:         newDoc.ID,
			MasterType:         masterDoc.Type,
			DocumentType:       newDoc.DocumentType,
			DocumentNumber:     newDoc.DocumentNumber,
			DocumentName:       newDoc.DocumentName,
			DocumentPath:       newDoc.FilePath,
			DocumentURL:        documentURL,
			FileSize:           newDoc.FileSize,
			MimeType:           newDoc.MimeType,
			VerificationStatus: newDoc.VerificationStatus,
			Message:            "Document uploaded successfully.",
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return response, nil
}
