// KEKS/CM -- KEKS-encoded cryptographic messages
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program.  If not, see <http://www.gnu.org/licenses/>.

package sign

import (
	"bytes"
	"crypto"
	"crypto/rand"
	"errors"
	"time"

	"github.com/google/uuid"

	"go.cypherpunks.su/keks"
	"go.cypherpunks.su/keks/cm"
	"go.cypherpunks.su/keks/cm/sign/mode"
	"go.cypherpunks.su/keks/schema"
)

const SignedMagic = keks.Magic("cm/signed")

type SDTBS struct {
	T string `keks:"t"`
}

type SigTBS struct {
	EncryptedTo [][]byte    `keks:"encrypted-to"`
	Exp         []time.Time `keks:"exp"`
	SID         []byte      `keks:"sid"`
	CID         uuid.UUID   `keks:"cid"`
}

func (sig *Sig) TBSGet() (*SigTBS, error) {
	var tbs SigTBS
	return &tbs, keks.Map2Struct(&tbs, sig.TBS)
}

type Sig struct {
	TBS  map[string]any `keks:"tbs"`
	Sign cm.AV          `keks:"sign"`
}

type Signed struct {
	TBS  SDTBS        `keks:"tbs"`
	Data *any       `keks:"data,omitempty"`
	Pubs *[]*Signed `keks:"pubs,omitempty"`
	Sigs []*Sig     `keks:"sigs,omitempty"`
}

// Parse and validate cm/signed from KEKS-encoded data.
func Parse(data []byte) (signed *Signed, tail []byte, err error) {
	{
		var magic keks.Magic
		magic, data = keks.StripMagic(data)
		if magic != "" && magic != SignedMagic {
			err = errors.New("wrong magic")
			return
		}
	}
	d := keks.NewDecoderFromBytes(data, nil)
	{
		var v any
		v, err = d.Decode()
		if err != nil {
			return
		}
		err = schema.Check("signed", SignedSchemas, v)
		if err != nil {
			return
		}
	}
	d = keks.NewDecoderFromBytes(data, nil)
	var sd Signed
	err = d.DecodeStruct(&sd)
	if err != nil {
		return
	}
	tail = d.B
	signed = &sd
	return
}

// Sign Signed's contents and sig-tbs corresponding data with the
// provided prv signer, having parent certififer. Signature is
// appended to the signed.Sigs. parent must have "sig" key-usage.
func (signed *Signed) SignWith(
	parent *PubData,
	prv Iface,
	sigTBS map[string]any,
) (err error) {
	if !parent.Can(KUSig) || len(parent.Pub) != 1 {
		return errors.New("parent can not sign")
	}
	sigTBS["sid"] = parent.Id
	var tbsRaw []byte
	if prv.Mode() == mode.Pure {
		var b bytes.Buffer
		if _, err = keks.Encode(&b, signed.Data, nil); err != nil {
			return
		}
		if _, err = keks.Encode(&b, signed.TBS, nil); err != nil {
			return
		}
		if _, err = keks.Encode(&b, sigTBS, nil); err != nil {
			return
		}
		tbsRaw = b.Bytes()
	} else {
		if _, err = keks.Encode(*prv.Prehasher(), signed.Data, nil); err != nil {
			return
		}
		if _, err = keks.Encode(*prv.Prehasher(), signed.TBS, nil); err != nil {
			return
		}
		if _, err = keks.Encode(*prv.Prehasher(), sigTBS, nil); err != nil {
			return
		}
		tbsRaw = (*prv.Prehasher()).Sum(nil)
	}
	sig := Sig{TBS: sigTBS}
	sig.Sign.A = prv.Algo()
	sig.Sign.V, err = prv.Sign(rand.Reader, tbsRaw, crypto.Hash(0))
	if err != nil {
		return
	}
	signed.Sigs = append(signed.Sigs, &sig)
	return nil
}
