// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package attestations

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"path"

	"github.com/gittuf/gittuf/internal/attestations/github"
	githubv01 "github.com/gittuf/gittuf/internal/attestations/github/v01"
	"github.com/gittuf/gittuf/internal/gitinterface"
	sslibdsse "github.com/gittuf/gittuf/internal/third_party/go-securesystemslib/dsse"
	gogithub "github.com/google/go-github/v61/github"
	ita "github.com/in-toto/attestation/go/v1"
)

func NewGitHubPullRequestAttestation(owner, repository string, pullRequestNumber int, commitID string, pullRequest *gogithub.PullRequest) (*ita.Statement, error) {
	return githubv01.NewPullRequestAttestation(owner, repository, pullRequestNumber, commitID, pullRequest)
}

func (a *Attestations) SetGitHubPullRequestAuthorization(repo *gitinterface.Repository, env *sslibdsse.Envelope, targetRefName, commitID string) error {
	envBytes, err := json.Marshal(env)
	if err != nil {
		return err
	}

	blobID, err := repo.WriteBlob(envBytes)
	if err != nil {
		return err
	}

	if a.githubPullRequestAttestations == nil {
		a.githubPullRequestAttestations = map[string]gitinterface.Hash{}
	}

	a.githubPullRequestAttestations[GitHubPullRequestAttestationPath(targetRefName, commitID)] = blobID
	return nil
}

// GitHubPullRequestAttestationPath constructs the expected path on-disk for the
// GitHub pull request attestation.
func GitHubPullRequestAttestationPath(refName, commitID string) string {
	return path.Join(refName, commitID)
}

// NewGitHubPullRequestApprovalAttestation creates a new GitHub pull request
// approval attestation for the provided information. The attestation is
// embedded in an in-toto "statement" and returned with the appropriate
// "predicate type" set. The `fromTargetID` and `toTargetID` specify the change
// to `targetRef` that is approved on the corresponding GitHub pull request.
func NewGitHubPullRequestApprovalAttestation(targetRef, fromRevisionID, targetTreeID string, approvers, dismissedApprovers []string) (*ita.Statement, error) {
	return githubv01.NewPullRequestApprovalAttestation(targetRef, fromRevisionID, targetTreeID, approvers, dismissedApprovers)
}

// SetGitHubPullRequestApprovalAttestation writes the new GitHub pull request
// approval attestation to the object store and tracks it in the current
// attestations state. The refName, fromRevisionID, targetTreeID parameters are
// used to construct an indexPath. The hostURL and reviewID are together mapped
// to the indexPath so that if the review is dismissed later, the corresponding
// attestation can be updated.
func (a *Attestations) SetGitHubPullRequestApprovalAttestation(repo *gitinterface.Repository, env *sslibdsse.Envelope, hostURL string, reviewID int64, appName, refName, fromRevisionID, targetTreeID string) error {
	// TODO: this will be updated to support validating different versions
	if err := githubv01.ValidatePullRequestApproval(env, refName, fromRevisionID, targetTreeID); err != nil {
		return errors.Join(github.ErrInvalidPullRequestApprovalAttestation, err)
	}

	envBytes, err := json.Marshal(env)
	if err != nil {
		return err
	}

	blobID, err := repo.WriteBlob(envBytes)
	if err != nil {
		return err
	}

	if a.codeReviewApprovalAttestations == nil {
		a.codeReviewApprovalAttestations = map[string]gitinterface.Hash{}
	}

	if a.codeReviewApprovalIndex == nil {
		a.codeReviewApprovalIndex = map[string]string{}
	}

	indexPath := GitHubPullRequestApprovalAttestationPath(refName, fromRevisionID, targetTreeID)
	// We URL encode the appName to make it appropriate for an on-disk path
	blobPath := path.Join(indexPath, base64.URLEncoding.EncodeToString([]byte(appName)))

	// Note the distinction between indexPath and blobPath
	// We don't have this for reference authorizations
	// indexPath is of the form "<ref>/<from commit>-<target tree>/github"
	// blobPath is a specific entry in the indexPath tree, for the app recording
	// the attestation

	a.codeReviewApprovalAttestations[blobPath] = blobID

	githubReviewID, err := GitHubReviewID(hostURL, reviewID)
	if err != nil {
		return err
	}
	if existingIndexPath, has := a.codeReviewApprovalIndex[githubReviewID]; has {
		if existingIndexPath != indexPath {
			return github.ErrInvalidPullRequestApprovalAttestation
		}
	} else {
		a.codeReviewApprovalIndex[githubReviewID] = indexPath // only use indexPath as the same review ID can be observed by more than one app
	}

	return nil
}

// GetGitHubPullRequestApprovalAttestationFor returns the requested GitHub pull
// request approval attestation. Here, all the pieces of information to load the
// attestation are known: the change the approval is for as well as the app that
// observed the approval.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationFor(repo *gitinterface.Repository, appName, refName, fromRevisionID, targetTreeID string) (*sslibdsse.Envelope, error) {
	indexPath := GitHubPullRequestApprovalAttestationPath(refName, fromRevisionID, targetTreeID)
	return a.GetGitHubPullRequestApprovalAttestationForIndexPath(repo, appName, indexPath)
}

// GetGitHubPullRequestApprovalAttestationForReviewID returns the requested
// GitHub pull request approval attestation for the specified GitHub instance,
// review ID, and app. This is used when the indexPath is unknown, such as when
// dismissing a prior approval. The host information and reviewID are used to
// identify the indexPath for the requested review.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationForReviewID(repo *gitinterface.Repository, hostURL string, reviewID int64, appName string) (*sslibdsse.Envelope, error) {
	indexPath, has, err := a.GetGitHubPullRequestApprovalIndexPathForReviewID(hostURL, reviewID)
	if err != nil {
		return nil, err
	}
	if has {
		return a.GetGitHubPullRequestApprovalAttestationForIndexPath(repo, appName, indexPath)
	}

	return nil, github.ErrGitHubReviewIDNotFound
}

// GetGitHubPullRequestApprovalAttestationForIndexPath returns the requested
// GitHub pull request approval attestation for the indexPath and appName.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationForIndexPath(repo *gitinterface.Repository, appName, indexPath string) (*sslibdsse.Envelope, error) {
	// We URL encode the appName to match the on-disk path
	blobPath := path.Join(indexPath, base64.URLEncoding.EncodeToString([]byte(appName)))
	blobID, has := a.codeReviewApprovalAttestations[blobPath]
	if !has {
		return nil, github.ErrPullRequestApprovalAttestationNotFound
	}

	envBytes, err := repo.ReadBlob(blobID)
	if err != nil {
		return nil, err
	}

	env := &sslibdsse.Envelope{}
	if err := json.Unmarshal(envBytes, env); err != nil {
		return nil, err
	}

	return env, nil
}

// GetGitHubPullRequestApprovalIndexPathForReviewID uses the host and review ID
// to find the previously recorded index path. Also see:
// SetGitHubPullRequestApprovalAttestation.
func (a *Attestations) GetGitHubPullRequestApprovalIndexPathForReviewID(hostURL string, reviewID int64) (string, bool, error) {
	githubReviewID, err := GitHubReviewID(hostURL, reviewID)
	if err != nil {
		return "", false, err
	}
	indexPath, has := a.codeReviewApprovalIndex[githubReviewID]
	return indexPath, has, nil
}

// GitHubPullRequestApprovalAttestationPath returns the expected path on-disk
// for the GitHub pull request approval attestation. This attestation type is
// stored using the same format as a reference authorization with the addition
// of `github` at the end of the path. This must be used as the tree to store
// specific attestation blobs in.
func GitHubPullRequestApprovalAttestationPath(refName, fromID, toID string) string {
	return path.Join(ReferenceAuthorizationPath(refName, fromID, toID), githubPullRequestApprovalSystemName)
}

// GitHubReviewID converts a GitHub specific review ID (recorded as an int64
// number by GitHub) into a code review system agnostic identifier used by
// gittuf.
func GitHubReviewID(hostURL string, reviewID int64) (string, error) {
	u, err := url.Parse(hostURL)
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%s::%d", u.Host, reviewID), nil
}
