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 SubmissionStatusIdToString(status int32) dbtypes.SubmissionStatus {
	switch status {
	case 0:
		return dbtypes.SubStatusUnspecified
	case 1:
		return dbtypes.SubStatusNew
	case 2:
		return dbtypes.SubStatusPending
	case 3:
		return dbtypes.SubStatusOnInterview
	case 4:
		return dbtypes.SubStatusRejected
	case 5:
		return dbtypes.SubStatusCancelled
	case 6:
		return dbtypes.SubStatusApproved
	default:
		return dbtypes.SubStatusUnspecified
	}
}

func SubmissionStatusStringToId(status dbtypes.SubmissionStatus) int32 {
	switch status {
	case dbtypes.SubStatusUnspecified:
		return 0
	case dbtypes.SubStatusNew:
		return 1
	case dbtypes.SubStatusPending:
		return 2
	case dbtypes.SubStatusOnInterview:
		return 3
	case dbtypes.SubStatusRejected:
		return 4
	case dbtypes.SubStatusCancelled:
		return 5
	case dbtypes.SubStatusApproved:
		return 6
	default:
		return 0
	}
}

//nolint:gocognit // not so hard
func (c *client) GetSubmissionList(
	ctx context.Context,
	request *dbtypes.SubmissionListGetRequest,
) (*dbtypes.SubmissionListGetResponse, error) {
	if request == nil {
		return nil, nil
	}

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

		submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
	)

	getSubmissions := psql.Select(
		"id", "uid", "vacancy_id", "submission_status_id",
		"cv", "name", "phone", "email", "birthday", "info",
	).From(submissionsTable).
		Limit(request.PageSize).
		Offset(countOffset(request.Page, request.PageSize))

	if request.Filters != nil {
		if request.Filters.AgentId != nil {
			getSubmissions = getSubmissions.Where(squirrel.Eq{"uid": *request.Filters.AgentId})
		}

		if request.Filters.VacancyId != nil {
			getSubmissions = getSubmissions.Where(squirrel.Eq{"vacancy_id": *request.Filters.VacancyId})
		}

		if request.Filters.Status != nil && *request.Filters.Status != dbtypes.SubStatusUnspecified {
			getSubmissions = getSubmissions.Where(squirrel.Eq{"submission_status_id": SubmissionStatusStringToId(*request.Filters.Status)})
		}
	}

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

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

	defer rows.Close()

	submissionsList := &dbtypes.SubmissionListGetResponse{
		Submissions: make([]dbtypes.Submission, 0, request.PageSize),
	}

	for rows.Next() {
		var (
			cvLink, info  sql.NullString
			statusId      int32
			candidateName string

			sub = dbtypes.Submission{
				AgentInfo:     new(dbtypes.AgentInfo),
				VacancyInfo:   new(dbtypes.VacancyInfo),
				CandidateInfo: new(dbtypes.CandidateInfo),
			}
		)

		if err := rows.Scan(
			&sub.Id, &sub.AgentInfo.Id, &sub.VacancyInfo.Id, &statusId,
			&cvLink, &candidateName, &sub.CandidateInfo.PhoneNumber, &sub.CandidateInfo.Email,
			&sub.CandidateInfo.Birthday, &info,
		); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return nil, dberrors.ErrNotFound
			}

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

		sub.CandidateInfo.LastName, sub.CandidateInfo.FirstName, sub.CandidateInfo.MiddleName = splitName(candidateName)

		if cvLink.Valid {
			sub.CandidateInfo.CvLink = &cvLink.String
		}

		if info.Valid {
			sub.CandidateInfo.Resume = &info.String
		}

		sub.Status = SubmissionStatusIdToString(statusId)

		submissionsList.Submissions = append(submissionsList.Submissions, sub)
	}

	return submissionsList, nil
}

func splitName(name string) (string, string, string) {
	var (
		parts                           = strings.SplitN(name, " ", 3)
		lastName, firstName, middleName string
	)

	if len(parts) > 0 {
		lastName = parts[0]
	}

	if len(parts) > 1 {
		firstName = parts[1]
	}

	if len(parts) > 2 {
		middleName = parts[2]
	}

	return lastName, firstName, middleName
}

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

	if request.CandidateInfo == nil {
		return nil, fmt.Errorf("%w: request.CandidateInfo is nil", dberrors.ErrBadRequest)
	}

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

		submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
	)

	var (
		// TODO: use normal uuid after DB reengineering
		subId    = fmt.Sprintf("%sSUB", strings.ReplaceAll(uuid.NewString(), "-", ""))
		fullName = formatName(request.CandidateInfo.LastName, request.CandidateInfo.FirstName, request.CandidateInfo.MiddleName)
	)

	createSubmission := psql.Insert(submissionsTable).
		Columns(
			"id", "uid", "vacancy_id", "submission_status_id",
			"cv", "name", "phone", "email", "birthday", "info",
		).
		Values(
			subId, request.AgentId, request.VacancyId, SubmissionStatusStringToId(dbtypes.SubStatusNew),
			request.CandidateInfo.CvLink, fullName, request.CandidateInfo.PhoneNumber,
			request.CandidateInfo.Email, request.CandidateInfo.Birthday, request.CandidateInfo.Resume,
		)

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

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

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

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

	return &dbtypes.SubmissionCreateResponse{
		Id: subId,
	}, nil
}

// NOTE: lastName and firstName are required to be non-empty
func formatName(lastName, firstName, middleName string) string {
	name := fmt.Sprintf("%s %s", lastName, firstName)

	if middleName != "" {
		name = fmt.Sprintf("%s %s", name, middleName)
	}

	return name
}

//nolint:funlen,gocognit // TODO: refactor
func (c *client) UpdateSubmissionStatus(
	ctx context.Context,
	request *dbtypes.SubmissionStatusUpdateRequest,
) (*dbtypes.SubmissionStatusUpdateResponse, error) {
	if request == nil {
		return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
	}

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

		submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
	)

	updateSubmissionStatus := psql.Update(submissionsTable).
		Set("submission_status_id", SubmissionStatusStringToId(request.Status)).
		Where(squirrel.Eq{"id": request.Id})

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

	if request.Status != dbtypes.SubStatusApproved {
		res, err := c.db.ExecContext(ctx, query, args...)
		if err != nil {
			return nil, fmt.Errorf("%w: error executing 'update submission status' query: %v", dberrors.ErrInternal, err)
		}

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

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

		return &dbtypes.SubmissionStatusUpdateResponse{}, nil
	}

	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() }()

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

	if vacancy.AgentReward == nil {
		return nil, fmt.Errorf("%w: agent reward is unknown", dberrors.ErrForbidden)
	}

	if vacancy.IsArchived {
		return nil, fmt.Errorf("%w: vacancy is closed", dberrors.ErrForbidden)
	}

	if vacancy.CurrentCandidates+1 > vacancy.RequiredCandidates {
		return nil, fmt.Errorf("%w: vacancy quota exceeded", dberrors.ErrForbidden)
	}

	if err := c.addSubmissionCandidate(ctx, tx, vacancy.Id); err != nil {
		return nil, fmt.Errorf("%w: error adding submission candidate: %v", dberrors.ErrInternal, err)
	}

	payload := dbtypes.TransactionPayload{
		Origin:      "submission",
		CompanyId:   vacancy.Company.Id,
		CompanyName: vacancy.Company.Name,
		VacancyId:   vacancy.Id,
		VacancyName: vacancy.Name,
	}

	molvaAgentTransId := uuid.NewString()

	if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
		OwnerId:   agentId,
		Amount:    int64(*vacancy.AgentReward),
		Currency:  "RUB",
		RequestId: molvaAgentTransId,
		Payload:   &payload,
	}); err != nil {
		return nil, fmt.Errorf("error creating molva -> agent transaction: %w", err)
	}

	distMolvaTransId := uuid.NewString()

	if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
		OwnerId:   vacancy.Company.Id,
		Amount:    -int64(*vacancy.AgentReward),
		Currency:  "RUB",
		RequestId: distMolvaTransId,
		Payload:   &payload,
	}); err != nil {
		return nil, fmt.Errorf("error creating dist -> molva transaction: %w", err)
	}

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

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

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

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

	return &dbtypes.SubmissionStatusUpdateResponse{}, nil
}

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

		submissionsTable       = fmt.Sprintf("%s.%s s", c.config.Schema, SubmissionsTableName)
		companiesTable         = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName)
		vacanciesTable         = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName)
		vacancyCandidatesTable = fmt.Sprintf("%s.%s vc", c.config.Schema, VacancyCandidatesTableName)
	)

	getMetadata := psql.Select(
		"s.uid", "v.id", "v.name", "v.company_id", "c.name", "v.agent_reward", "v.is_archived",
		"vc.current_candidates_amt", "vc.required_candidates_amt",
	).From(submissionsTable).
		InnerJoin(fmt.Sprintf("%s on s.vacancy_id = v.id", vacanciesTable)).
		InnerJoin(fmt.Sprintf("%s on v.id = vc.vacancy_id", vacancyCandidatesTable)).
		InnerJoin(fmt.Sprintf("%s on v.company_id = c.id", companiesTable)).
		Where(squirrel.Eq{"s.id": submissionId}).
		Suffix("FOR UPDATE OF vc")

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

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

	var (
		agentId string
		vacancy dbtypes.Vacancy
	)

	if err := row.Scan(
		&agentId,
		&vacancy.Id, &vacancy.Name, &vacancy.Company.Id, &vacancy.Company.Name,
		&vacancy.AgentReward, &vacancy.IsArchived,
		&vacancy.CurrentCandidates, &vacancy.RequiredCandidates,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, "", dberrors.ErrNotFound
		}

		return nil, "", fmt.Errorf("%w: error scanning row for 'get submission metadata' query: %v", dberrors.ErrInternal, err)
	}

	return &vacancy, agentId, nil
}

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

		vacancyCandisTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyCandidatesTableName)
	)

	addSubmissionCandidate := psql.Update(vacancyCandisTable).
		Set("current_candidates_amt", squirrel.Expr("current_candidates_amt + 1")).
		Where(squirrel.Eq{"vacancy_id": vacancyId})

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

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

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

	if rowsAffected == 0 {
		return dberrors.ErrInternal
	}

	return nil
}

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

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

		submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
	)

	deleteSubmission := psql.Update(submissionsTable).
		Set("submission_status_id", SubmissionStatusStringToId(dbtypes.SubStatusCancelled)).
		Where(squirrel.Eq{"id": request.Id})

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

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

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

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

	return &dbtypes.SubmissionDeleteResponse{}, nil
}
