package usecase

import (
	"context"
	"errors"
	"fmt"
	"git.sch9.ru/new_gate/ms-tester/internal/models"
	"git.sch9.ru/new_gate/ms-tester/internal/tester"
	"git.sch9.ru/new_gate/ms-tester/pkg"
	"github.com/microcosm-cc/bluemonday"
	"strings"
)

type ProblemUseCase struct {
	problemRepo  tester.ProblemPostgresRepository
	pandocClient pkg.PandocClient
}

func NewProblemUseCase(
	problemRepo tester.ProblemPostgresRepository,
	pandocClient pkg.PandocClient,
) *ProblemUseCase {
	return &ProblemUseCase{
		problemRepo:  problemRepo,
		pandocClient: pandocClient,
	}
}

func (u *ProblemUseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
	return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
}

func (u *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
	return u.problemRepo.ReadProblemById(ctx, u.problemRepo.DB(), id)
}

func (u *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error {
	return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
}

func (u *ProblemUseCase) ListProblems(ctx context.Context, page int32, pageSize int32) ([]*models.ProblemsListItem, int32, error) {
	return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), page, pageSize)
}

func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error {
	if isEmpty(problemUpdate) {
		return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update")
	}

	tx, err := u.problemRepo.BeginTx(ctx)
	if err != nil {
		return err
	}

	problem, err := u.problemRepo.ReadProblemById(ctx, tx, id)
	if err != nil {
		return errors.Join(err, tx.Rollback())
	}

	statement := models.ProblemStatement{
		Legend:       problem.Legend,
		InputFormat:  problem.InputFormat,
		OutputFormat: problem.OutputFormat,
		Notes:        problem.Notes,
		Scoring:      problem.Scoring,
	}

	if problemUpdate.Legend != nil {
		statement.Legend = *problemUpdate.Legend
	}
	if problemUpdate.InputFormat != nil {
		statement.InputFormat = *problemUpdate.InputFormat
	}
	if problemUpdate.OutputFormat != nil {
		statement.OutputFormat = *problemUpdate.OutputFormat
	}
	if problemUpdate.Notes != nil {
		statement.Notes = *problemUpdate.Notes
	}
	if problemUpdate.Scoring != nil {
		statement.Scoring = *problemUpdate.Scoring
	}

	builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
	if err != nil {
		return errors.Join(err, tx.Rollback())
	}

	if builtStatement.LegendHtml != problem.LegendHtml {
		problemUpdate.LegendHtml = &builtStatement.LegendHtml
	}
	if builtStatement.InputFormatHtml != problem.InputFormatHtml {
		problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml
	}
	if builtStatement.OutputFormatHtml != problem.OutputFormatHtml {
		problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml
	}
	if builtStatement.NotesHtml != problem.NotesHtml {
		problemUpdate.NotesHtml = &builtStatement.NotesHtml
	}
	if builtStatement.ScoringHtml != problem.ScoringHtml {
		problemUpdate.ScoringHtml = &builtStatement.ScoringHtml
	}

	err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)
	if err != nil {
		return errors.Join(err, tx.Rollback())
	}

	err = tx.Commit()
	if err != nil {
		return err
	}

	return nil
}

func isEmpty(p models.ProblemUpdate) bool {
	return p.Title == nil &&
		p.Legend == nil &&
		p.InputFormat == nil &&
		p.OutputFormat == nil &&
		p.Notes == nil &&
		p.Scoring == nil &&
		p.MemoryLimit == nil &&
		p.TimeLimit == nil
}

func wrap(s string) string {
	return fmt.Sprintf("\\begin{document}\n%s\n\\end{document}\n", s)
}

func trimSpaces(statement models.ProblemStatement) models.ProblemStatement {
	return models.ProblemStatement{
		Legend:       strings.TrimSpace(statement.Legend),
		InputFormat:  strings.TrimSpace(statement.InputFormat),
		OutputFormat: strings.TrimSpace(statement.OutputFormat),
		Notes:        strings.TrimSpace(statement.Notes),
		Scoring:      strings.TrimSpace(statement.Scoring),
	}
}

func sanitize(statement models.Html5ProblemStatement) models.Html5ProblemStatement {
	p := bluemonday.UGCPolicy()

	p.AllowAttrs("class").Globally()
	p.AllowAttrs("style").Globally()
	p.AllowStyles("text-align").MatchingEnum("center", "left", "right").Globally()
	p.AllowStyles("display").MatchingEnum("block", "inline", "inline-block").Globally()

	p.AllowStandardURLs()
	p.AllowAttrs("cite").OnElements("blockquote", "q")
	p.AllowAttrs("href").OnElements("a", "area")
	p.AllowAttrs("src").OnElements("img")

	if statement.LegendHtml != "" {
		statement.LegendHtml = p.Sanitize(statement.LegendHtml)
	}
	if statement.InputFormatHtml != "" {
		statement.InputFormatHtml = p.Sanitize(statement.InputFormatHtml)
	}
	if statement.OutputFormatHtml != "" {
		statement.OutputFormatHtml = p.Sanitize(statement.OutputFormatHtml)
	}
	if statement.NotesHtml != "" {
		statement.NotesHtml = p.Sanitize(statement.NotesHtml)
	}
	if statement.ScoringHtml != "" {
		statement.ScoringHtml = p.Sanitize(statement.ScoringHtml)
	}

	return statement
}

func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) {
	p = trimSpaces(p)

	latex := models.ProblemStatement{}

	if p.Legend != "" {
		latex.Legend = wrap(p.Legend)
	}
	if p.InputFormat != "" {
		latex.InputFormat = wrap(p.InputFormat)
	}
	if p.OutputFormat != "" {
		latex.OutputFormat = wrap(p.OutputFormat)
	}
	if p.Notes != "" {
		latex.Notes = wrap(p.Notes)
	}
	if p.Scoring != "" {
		latex.Scoring = wrap(p.Scoring)
	}

	req := []string{
		latex.Legend,
		latex.InputFormat,
		latex.OutputFormat,
		latex.Notes,
		latex.Scoring,
	}

	res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req)
	if err != nil {
		return models.Html5ProblemStatement{}, err
	}

	if len(res) != len(req) {
		return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res))
	}

	sanitizedStatement := sanitize(models.Html5ProblemStatement{
		LegendHtml:       res[0],
		InputFormatHtml:  res[1],
		OutputFormatHtml: res[2],
		NotesHtml:        res[3],
		ScoringHtml:      res[4],
	})

	return sanitizedStatement, nil
}