package pgdb

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"

	dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
	dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
	"github.com/Masterminds/squirrel"
	"github.com/google/uuid"
)

func VacancyStatusIdToString(status int32) dbtypes.VacancyStatus {
	switch status {
	case 0:
		return dbtypes.VacUnspecified
	case 1:
		return dbtypes.VacNew
	case 2:
		return dbtypes.VacPending
	case 3:
		return dbtypes.VacApproved
	case 4:
		return dbtypes.VacRejected
	default:
		return dbtypes.VacUnspecified
	}
}

func VacancyStatusStringToId(status dbtypes.VacancyStatus) int32 {
	switch status {
	case dbtypes.VacUnspecified:
		return 0
	case dbtypes.VacNew:
		return 1
	case dbtypes.VacPending:
		return 2
	case dbtypes.VacApproved:
		return 3
	case dbtypes.VacRejected:
		return 4
	default:
		return 0
	}
}

//nolint:funlen,gocognit // maybe refactor later
func (c *client) GetVacancyList(
	ctx context.Context,
	request *dbtypes.VacancyListGetRequest,
) (*dbtypes.VacancyListGetResponse, error) {
	if request == nil {
		return nil, nil
	}

	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName)
		companyTable = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName)
	)

	getVacList := psql.Select(
		"v.id", "v.company_id", "c.name", "v.name", "v.address", "v.work_format",
		"v.agent_reward", "v.region", "v.salary_top", "v.salary_bottom", "v.requirements",
		"v.responsibilities", "v.additional_info", "v.is_archived",
		"v.target_action", "v.target_action_amount", "v.publish_date",
		"v.moderation_status_id", "v.required_candidates", "v.current_candidates",
		"v.additional_fields",
	).
		From(vacancyTable).
		LeftJoin(fmt.Sprintf("%s on v.company_id = c.id", companyTable)).
		Limit(request.PageSize).
		Offset(countOffset(request.Page, request.PageSize))

	getVacList = c.setVacancyListFilters(getVacList, request.Filters)

	query, args, err := getVacList.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building get vacancy list query: %v", dberrors.ErrInternal, err)
	}

	rows, err := c.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("%w: error executing get vacancy list query: %v", dberrors.ErrInternal, err)
	}

	defer rows.Close()

	var (
		res        dbtypes.VacancyListGetResponse
		vacancyIds = make([]string, 0, request.PageSize)
	)

	for rows.Next() {
		var (
			status                                       int32
			agentReward                                  sql.NullInt32
			comName, reqs, resps, extraInfo, extraFields sql.NullString
			vacancy                                      dbtypes.Vacancy
		)

		if err := rows.Scan(
			&vacancy.Id, &vacancy.Company.Id, &comName, &vacancy.Name, &vacancy.Address,
			&vacancy.WorkFormat, &agentReward, &vacancy.Region, &vacancy.SalaryTop, &vacancy.SalaryBottom,
			&reqs, &resps, &extraInfo, &vacancy.IsArchived, &vacancy.TargetAction.Action, &vacancy.TargetAction.Duration,
			&vacancy.CreatedAt, &status, &vacancy.RequiredCandidates, &vacancy.CurrentCandidates,
			&extraFields,
		); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return nil, dberrors.ErrNotFound
			}

			return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
		}

		if agentReward.Valid {
			vacancy.AgentReward = &agentReward.Int32
		}

		if comName.Valid {
			vacancy.Company.Name = comName.String
		}

		if reqs.Valid {
			vacancy.Requirements = &reqs.String
		}

		if resps.Valid {
			vacancy.Responsibilities = &resps.String
		}

		if extraInfo.Valid {
			vacancy.ExtraInfo = &extraInfo.String
		}

		if extraFields.Valid {
			vacancy.ExtraFields = &extraFields.String
		}

		vacancy.Moderation.Status = VacancyStatusIdToString(status)

		res.Vacancies = append(res.Vacancies, vacancy)
		vacancyIds = append(vacancyIds, vacancy.Id)
	}

	descriptions, err := c.getVacancyModerationDescriptionHistory(ctx, c.db, vacancyIds)
	if err != nil {
		if errors.Is(err, dberrors.ErrNotFound) {
			return &res, nil
		}

		return nil, fmt.Errorf("error getting descriptions for vacancies: %w", err)
	}

	for i, vacancy := range res.Vacancies {
		res.Vacancies[i].Moderation.DescriptionHistory = descriptions[vacancy.Id]
	}

	return &res, nil
}

func (c *client) setVacancyListFilters(
	query squirrel.SelectBuilder,
	filters *dbtypes.VacancyListFilters,
) squirrel.SelectBuilder {
	if filters.DistributorId != nil {
		query = query.Where(squirrel.Eq{"c.uid": *filters.DistributorId})
	}

	if filters.CompanyId != nil {
		query = query.Where(squirrel.Eq{"v.company_id": *filters.CompanyId})
	}

	if filters.VacancyId != nil {
		query = query.Where(squirrel.Eq{"v.id": *filters.VacancyId})
	}

	if filters.Region != nil {
		query = query.Where(squirrel.Eq{"v.region": *filters.Region})
	}

	if filters.SalaryBottom != nil {
		query = query.Where(squirrel.Or{
			squirrel.GtOrEq{"v.salary_bottom": *filters.SalaryBottom},
			squirrel.Eq{"v.salary_bottom": nil},
		})
	}

	if filters.SalaryTop != nil {
		query = query.Where(squirrel.Or{
			squirrel.LtOrEq{"v.salary_top": *filters.SalaryTop},
			squirrel.Eq{"v.salary_top": nil},
		})
	}

	if filters.IsArchived != nil {
		query = query.Where(squirrel.Eq{"v.is_archived": *filters.IsArchived})
	}

	if filters.Status != nil {
		query = query.Where(squirrel.Eq{"v.moderation_status_id": VacancyStatusStringToId(*filters.Status)})
	}

	return query
}

func (c *client) getVacancyModerationDescriptionHistory(
	ctx context.Context,
	driver Driver,
	vacancyIds []string,
) (map[string][]dbtypes.VacancyModerationDescription, error) {
	if len(vacancyIds) == 0 {
		return nil, nil
	}

	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyModerDescriptionsTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyDescriptionHistoryTableName)
	)

	getVacancyModerationDescriptionHistory := psql.Select(
		"vacancy_id",
		"description",
		"created_at",
	).
		From(vacancyModerDescriptionsTable).
		Where(squirrel.Eq{"vacancy_id": vacancyIds}).
		OrderBy("vacancy_id", "created_at ASC")

	query, args, err := getVacancyModerationDescriptionHistory.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err)
	}

	rows, err := driver.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("%w: error executing getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err)
	}

	defer rows.Close()

	descriptionsMap := make(map[string][]dbtypes.VacancyModerationDescription)

	for rows.Next() {
		var (
			vacancyId   string
			description dbtypes.VacancyModerationDescription
		)

		if err := rows.Scan(
			&vacancyId,
			&description.Description,
			&description.CreatedAt,
		); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return nil, dberrors.ErrNotFound
			}

			return nil, fmt.Errorf("%w: error scanning row for getVacancyModerationDescriptionHistory: %v", dberrors.ErrInternal, err)
		}

		descriptionsMap[vacancyId] = append(descriptionsMap[vacancyId], description)
	}

	return descriptionsMap, nil
}

func (c *client) CreateVacancy(
	ctx context.Context,
	request *dbtypes.VacancyCreateRequest,
) (*dbtypes.VacancyCreateResponse, error) {
	if request == nil {
		return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
	}

	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
	)

	// TODO: use normal uuid after DB reengineering
	vacancyId := fmt.Sprintf("%sVAC", strings.ReplaceAll(uuid.NewString(), "-", ""))

	createVacancy := psql.Insert(vacancyTable).
		Columns(
			"id", "company_id", "name", "address", "work_format", "agent_reward",
			"salary_top", "salary_bottom", "requirements", "responsibilities", "additional_info",
			"region", "is_archived", "target_action", "target_action_amount", "moderation_status_id",
			"required_candidates", "publish_date", "current_candidates", "additional_fields",
		).
		Values(
			vacancyId, request.CompanyId, request.Name, request.Address, request.WorkFormat, request.AgentReward,
			request.SalaryTop, request.SalaryBottom, request.Requirements, request.Responsibilities, request.ExtraInfo,
			request.Region, false, request.TargetAction.Action, request.TargetAction.Duration, VacancyStatusStringToId(dbtypes.VacNew),
			request.RequiredCandidates, squirrel.Expr("now()"), request.CurrentCandidates, request.ExtraFields,
		)

	query, args, err := createVacancy.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building create vacancy query: %v", dberrors.ErrInternal, err)
	}

	res, err := c.db.ExecContext(ctx, query, args...)
	if err != nil {
		// TODO: process conflict via err.(*pq.Error).Code
		return nil, fmt.Errorf("%w: error executing create vacancy query: %v", dberrors.ErrInternal, err)
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("%w: error getting rows affected for create vacancy query: %v", dberrors.ErrInternal, err)
	}

	if rowsAffected == 0 {
		return nil, dberrors.ErrInternal
	}

	return &dbtypes.VacancyCreateResponse{Id: vacancyId}, nil
}

func (c *client) UpdateVacancy(
	ctx context.Context,
	request *dbtypes.VacancyUpdateRequest,
) (*dbtypes.VacancyUpdateResponse, error) {
	if request == nil {
		return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
	}

	tx, err := c.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
	}

	defer func() { _ = tx.Rollback() }()

	status, err := c.getVacancyModerStatusForUpdate(ctx, tx, request.Id)
	if err != nil {
		return nil, fmt.Errorf("error getting vacancy moder status: %w", err)
	}

	if status == dbtypes.VacApproved {
		return nil, dberrors.ErrForbidden
	}

	res, err := c.updateVacancy(ctx, tx, request)
	if err != nil {
		return nil, fmt.Errorf("error updating vacancy: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("error committing transaction: %w", err)
	}

	return res, nil
}

func (c *client) getVacancyModerStatusForUpdate(
	ctx context.Context,
	driver Driver,
	vacancyId string,
) (dbtypes.VacancyStatus, error) {
	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
	)

	getVacancyModerStatus := psql.Select(
		"moderation_status_id",
	).
		From(vacancyTable).
		Where(squirrel.Eq{"id": vacancyId}).
		Suffix("FOR UPDATE")

	query, args, err := getVacancyModerStatus.ToSql()
	if err != nil {
		return "", fmt.Errorf("%w: error building get vacancy moder status query: %v", dberrors.ErrInternal, err)
	}

	row := driver.QueryRowContext(ctx, query, args...)

	var (
		statusId int32
	)

	if err := row.Scan(&statusId); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return "", dberrors.ErrNotFound
		}

		return "", fmt.Errorf("%w: error scanning row for get vacancy moder status query: %v", dberrors.ErrInternal, err)
	}

	return VacancyStatusIdToString(statusId), nil
}

func (c *client) updateVacancy(
	ctx context.Context,
	driver Driver,
	request *dbtypes.VacancyUpdateRequest,
) (*dbtypes.VacancyUpdateResponse, error) {
	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
	)

	updateVacancy := psql.Update(vacancyTable).
		Where(squirrel.Eq{"id": request.Id})

	if request.Name != nil {
		updateVacancy = updateVacancy.Set("name", *request.Name)
	}

	if request.Address != nil {
		updateVacancy = updateVacancy.Set("address", *request.Address)
	}

	if request.WorkFormat != nil {
		updateVacancy = updateVacancy.Set("work_format", *request.WorkFormat)
	}

	if request.AgentReward != nil {
		updateVacancy = updateVacancy.Set("agent_reward", *request.AgentReward)
	}

	if request.SalaryTop != nil {
		updateVacancy = updateVacancy.Set("salary_top", *request.SalaryTop)
	}

	if request.SalaryBottom != nil {
		updateVacancy = updateVacancy.Set("salary_bottom", *request.SalaryBottom)
	}

	if request.Requirements != nil {
		updateVacancy = updateVacancy.Set("requirements", *request.Requirements)
	}

	if request.Responsibilities != nil {
		updateVacancy = updateVacancy.Set("responsibilities", *request.Responsibilities)
	}

	if request.ExtraInfo != nil {
		updateVacancy = updateVacancy.Set("extra_info", *request.ExtraInfo)
	}

	if request.Region != nil {
		updateVacancy = updateVacancy.Set("region", *request.Region)
	}

	if request.TargetAction.Action != nil {
		updateVacancy = updateVacancy.Set("target_action", *request.TargetAction.Action)
	}

	if request.TargetAction.Duration != nil {
		updateVacancy = updateVacancy.Set("target_action_amount", *request.TargetAction.Duration)
	}

	if request.RequiredCandidates != nil {
		updateVacancy = updateVacancy.Set("required_candidates", *request.RequiredCandidates)
	}

	if request.ExtraFields != nil {
		updateVacancy = updateVacancy.Set("additional_fields", *request.ExtraFields)
	}

	query, args, err := updateVacancy.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building update vacancy query: %v", dberrors.ErrInternal, err)
	}

	res, err := driver.ExecContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("%w: error executing update vacancy query: %v", dberrors.ErrInternal, err)
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("%w: error getting rows affected for update vacancy query: %v", dberrors.ErrInternal, err)
	}

	if rowsAffected == 0 {
		return nil, dberrors.ErrInternal
	}

	return &dbtypes.VacancyUpdateResponse{}, nil
}

func (c *client) DeleteVacancy(
	ctx context.Context,
	request *dbtypes.VacancyDeleteRequest,
) (*dbtypes.VacancyDeleteResponse, error) {
	if request == nil {
		return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
	}

	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
	)

	deleteVacancy := psql.Update(vacancyTable).
		Set("is_archived", true).
		Where(squirrel.Eq{"id": request.Id})

	query, args, err := deleteVacancy.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building delete vacancy query: %v", dberrors.ErrInternal, err)
	}

	res, err := c.db.ExecContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("%w: error executing delete vacancy query: %v", dberrors.ErrInternal, err)
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("%w: error getting rows affected for delete vacancy query: %v", dberrors.ErrInternal, err)
	}

	if rowsAffected == 0 {
		return nil, dberrors.ErrNotFound
	}

	return &dbtypes.VacancyDeleteResponse{}, nil
}

func (c *client) SendVacancyToModeration(
	ctx context.Context,
	request *dbtypes.SendVacancyToModerationRequest,
) (*dbtypes.SendVacancyToModerationResponse, error) {
	if request == nil {
		return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
	}

	var (
		psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

		vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
	)

	sendVacancyToModeration := psql.Update(vacancyTable).
		Set("moderation_status_id", VacancyStatusStringToId(dbtypes.VacPending)).
		Where(squirrel.Eq{"id": request.Id})

	query, args, err := sendVacancyToModeration.ToSql()
	if err != nil {
		return nil, fmt.Errorf("%w: error building send vacancy to moderation query: %v", dberrors.ErrInternal, err)
	}

	res, err := c.db.ExecContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("%w: error executing send vacancy to moderation query: %v", dberrors.ErrInternal, err)
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("%w: error getting rows affected for send vacancy to moderation query: %v", dberrors.ErrInternal, err)
	}

	if rowsAffected == 0 {
		return nil, dberrors.ErrNotFound
	}

	return &dbtypes.SendVacancyToModerationResponse{}, nil
}
