package common

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"time"

	"party.at/party/internal/data"
	"party.at/party/internal/validator"
)

type IssueDetail struct {
	data.Issue
	CanVote bool `json:"can_vote"`
}

type OptionResult struct {
	data.Option
	VoteCount int64 `json:"vote_count"`
}

type IssueWithOptions struct {
	IssueDetail
	Options []OptionResult
}

type PublicKey struct {
	N string `json:"n"`
	E int    `json:"e"`
}

func (app *Application) FetchIssues(title string, filters data.Filters, user *data.User) ([]IssueDetail, data.Metadata, error) {
	issues, metadata, err := app.Models.Issues.GetAll(title, filters)
	if err != nil {
		return nil, data.Metadata{}, err
	}

	details := make([]IssueDetail, len(issues))
	for i, issue := range issues {
		canVote := false
		if !user.IsAnonymous() {
			_, err = app.Models.BlindSigns.Get(user.ID, issue.ID)
			switch {
			case errors.Is(err, data.ErrRecordNotFound):
				canVote = true
			case err != nil:
				return nil, data.Metadata{}, err
			}
		}
		details[i] = IssueDetail{Issue: *issue, CanVote: canVote}
	}

	return details, metadata, nil
}

func (app *Application) CreateIssue(title, description string, startTime, endTime time.Time, optionLabels []string) (*data.Issue, []*data.Option, error) {
	issue := &data.Issue{
		Title:       title,
		Description: description,
		StartTime:   startTime,
		EndTime:     endTime,
	}

	options := make([]*data.Option, len(optionLabels))
	for i, label := range optionLabels {
		options[i] = &data.Option{Label: label}
	}

	v := validator.New()
	v.Check(len(optionLabels) >= 2, "options", "must provide at least two options")
	v.Check(len(optionLabels) <= 10, "options", "must not provide more than ten options")
	for _, option := range options {
		data.ValidateOption(v, option)
	}
	if data.ValidateIssue(v, issue); !v.Valid() {
		return nil, nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
	}

	n, e, privatePEM, err := generateIssueKey()
	if err != nil {
		return nil, nil, err
	}
	issue.N = n
	issue.E = e
	issue.PrivatePem = privatePEM

	if err = app.Models.Issues.InsertWithOptions(issue, options); err != nil {
		return nil, nil, err
	}

	return issue, options, nil
}

func (app *Application) GetIssue(id int64, user *data.User) (*IssueWithOptions, error) {
	issue, err := app.Models.Issues.Get(id)
	if err != nil {
		return nil, err
	}

	canVote := false
	if !user.IsAnonymous() {
		_, err = app.Models.BlindSigns.Get(user.ID, issue.ID)
		switch {
		case errors.Is(err, data.ErrRecordNotFound):
			canVote = true
		case err != nil:
			return nil, err
		}
	}

	options, err := app.Models.Options.GetAllForIssue(id)
	if err != nil {
		return nil, err
	}

	results := make([]OptionResult, len(options))
	for i, o := range options {
		voteCount, err := app.Models.Votes.CountForOption(o.ID)
		if err != nil {
			return nil, err
		}
		results[i] = OptionResult{Option: *o, VoteCount: voteCount}
	}

	return &IssueWithOptions{
		IssueDetail: IssueDetail{Issue: *issue, CanVote: canVote},
		Options:     results,
	}, nil
}

func (app *Application) UpdateIssue(id int64, title, description *string, startTime, endTime *time.Time) (*data.Issue, error) {
	issue, err := app.Models.Issues.Get(id)
	if err != nil {
		return nil, err
	}

	if title != nil       { issue.Title = *title }
	if description != nil { issue.Description = *description }
	if startTime != nil   { issue.StartTime = *startTime }
	if endTime != nil     { issue.EndTime = *endTime }

	v := validator.New()
	if data.ValidateIssue(v, issue); !v.Valid() {
		return nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
	}

	if err = app.Models.Issues.Update(issue); err != nil {
		return nil, err
	}

	return issue, nil
}

func (app *Application) DeleteIssue(id int64) error {
	return app.Models.Issues.Delete(id)
}

func (app *Application) GetIssuePublicKey(id int64) (*PublicKey, error) {
	issue, err := app.Models.Issues.Get(id)
	if err != nil {
		return nil, err
	}
	return &PublicKey{N: hex.EncodeToString(issue.N), E: issue.E}, nil
}

func (app *Application) BlindSign(issueID int64, blindedVote []byte, user *data.User) ([]byte, error) {	
	issue, err := app.Models.Issues.Get(issueID)
	if err != nil {
		return nil, err
	}

	if issue.StartTime.After(time.Now()) {
		return nil, data.ErrHasNotStarted
	}

	blindSign := &data.BlindSign{UserID: user.ID, IssueID: issue.ID}
	if err = app.Models.BlindSigns.Insert(blindSign); err != nil {
		return nil, err
	}

	return app.Models.BlindSigns.BlindSign(issue.ID, blindedVote)
}

func generateIssueKey() ([]byte, int, []byte, error) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, 0, nil, err
	}
	privDER := x509.MarshalPKCS1PrivateKey(key)
	privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDER})
	return key.N.Bytes(), key.E, privPEM, nil
}
