package gitcode

import (
	"encoding/json"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"
)

type IssueListRequest struct {
	Owner     string
	Repo      string
	State     string
	Labels    []string
	OrderBy   string
	Direction string
	Page      int
	PerPage   int
}

type IssueRequest struct {
	Owner            string
	Repo             string
	Number           int
	KnownRemoteAlias bool
	RemoteAlias      string
}

type RepositoryIssueCommentListRequest struct {
	Owner   string
	Repo    string
	Page    int
	PerPage int
}

type PRListRequest struct {
	Owner     string
	Repo      string
	State     string
	OrderBy   string
	Direction string
	Page      int
	PerPage   int
}

type PRRequest struct {
	Owner  string
	Repo   string
	Number int
}

type CreatePRRequest struct {
	Owner string `json:"-"`
	Repo  string `json:"-"`
	Title string `json:"title"`
	Body  string `json:"body,omitempty"`
	Head  string `json:"head"`
	Base  string `json:"base"`
}

type UpdatePRRequest struct {
	Owner  string `json:"-"`
	Repo   string `json:"-"`
	Number int    `json:"-"`
	Title  string `json:"title,omitempty"`
	Body   string `json:"body,omitempty"`
	State  string `json:"state,omitempty"`
}

type MergePRRequest struct {
	Owner       string `json:"-"`
	Repo        string `json:"-"`
	Number      int    `json:"-"`
	HeadSHA     string `json:"-"`
	MergeMethod string `json:"merge_method,omitempty"`
}

type MergePRResponse struct {
	SHA     string `json:"sha"`
	Merged  any    `json:"merged"`
	Message string `json:"message"`
}

type CreatePRCommentRequest struct {
	Owner  string `json:"-"`
	Repo   string `json:"-"`
	Number int    `json:"-"`
	Body   string `json:"body"`
}

type CreatePRReviewCommentRequest struct {
	Owner     string `json:"-"`
	Repo      string `json:"-"`
	Number    int    `json:"-"`
	Body      string `json:"body"`
	Path      string `json:"path"`
	Line      int    `json:"line,omitempty"`
	Position  int    `json:"position,omitempty"`
	StartLine int    `json:"start_line,omitempty"`
	EndLine   int    `json:"end_line,omitempty"`
}

type ReplyPRReviewCommentRequest struct {
	Owner           string `json:"-"`
	Repo            string `json:"-"`
	Number          int    `json:"-"`
	DiscussionID    string `json:"-"`
	ParentCommentID string `json:"-"`
	Body            string `json:"body"`
}

type LinkPRIssueRequest struct {
	Owner       string `json:"-"`
	Repo        string `json:"-"`
	Number      int    `json:"-"`
	IssueNumber int    `json:"-"`
}

type WikiPageRequest struct {
	Owner string
	Repo  string
	Slug  string
	Path  string
}

type WikiListRequest struct {
	Owner   string
	Repo    string
	Page    int
	PerPage int
	Bounds  *WikiBounds
}

type WikiBounds struct {
	MaxRecords   int
	MaxBytes     int64
	OffsetPaging bool
	ProgressChan chan<- WikiProgressEvent
}

type WikiProgressEvent struct {
	Path           string
	RecordsFetched int
}

type SearchRequest struct {
	Query   string
	Owner   string
	Repo    string
	Type    string
	Page    int
	PerPage int
}

type AttachmentRequest struct {
	Owner        string
	Repo         string
	IssueNumber  int
	AttachmentID string
	Name         string
}

type Page[T any] struct {
	Items      []T
	Page       int
	PerPage    int
	TotalCount int
	NextPage   int
}

type GitCodeLabel struct {
	ID        int64  `json:"id"`
	Name      string `json:"name"`
	Color     string `json:"color"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

type IssueSummary struct {
	ID            string         `json:"id"`
	Number        int            `json:"number"`
	Title         string         `json:"title"`
	Body          string         `json:"body"`
	Status        string         `json:"status"`
	State         string         `json:"state"`
	Comments      int            `json:"comments"`
	Milestone     *Milestone     `json:"milestone,omitempty"`
	GitCodeLabels []GitCodeLabel `json:"labels"`
	Labels        []string       `json:"-"`
	CreatedAt     time.Time      `json:"created_at"`
	UpdatedAt     time.Time      `json:"updated_at"`
}

func (i *IssueSummary) UnmarshalJSON(data []byte) error {
	var raw struct {
		ID        any             `json:"id"`
		Number    any             `json:"number"`
		Title     string          `json:"title"`
		Body      string          `json:"body"`
		Status    string          `json:"status"`
		State     string          `json:"state"`
		Comments  any             `json:"comments"`
		Milestone *Milestone      `json:"milestone"`
		Labels    json.RawMessage `json:"labels"`
		CreatedAt time.Time       `json:"created_at"`
		UpdatedAt time.Time       `json:"updated_at"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	var err error
	i.ID, err = decodeID(raw.ID)
	if err != nil {
		return err
	}
	i.Number, err = decodeNumber(raw.Number)
	if err != nil {
		return err
	}
	i.Title = raw.Title
	i.Body = raw.Body
	i.Status = raw.Status
	i.State = raw.State
	i.Comments, err = decodeOptionalInt(raw.Comments)
	if err != nil {
		return err
	}
	i.Milestone = raw.Milestone
	if len(raw.Labels) > 0 {
		if isLabelObjectArray(raw.Labels) {
			if err := json.Unmarshal(raw.Labels, &i.GitCodeLabels); err != nil {
				return err
			}
			i.Labels, err = NormalizeLabels(i.GitCodeLabels)
			if err != nil {
				return err
			}
		} else {
			var strs []string
			if err := json.Unmarshal(raw.Labels, &strs); err != nil {
				return err
			}
			i.Labels = strs
		}
	}
	i.CreatedAt = raw.CreatedAt
	i.UpdatedAt = raw.UpdatedAt
	return nil
}

type Issue struct {
	ID            string         `json:"id"`
	Number        int            `json:"number"`
	Title         string         `json:"title"`
	Body          string         `json:"body"`
	Status        string         `json:"status"`
	State         string         `json:"state"`
	Comments      int            `json:"comments"`
	Milestone     *Milestone     `json:"milestone,omitempty"`
	GitCodeLabels []GitCodeLabel `json:"labels"`
	Labels        []string       `json:"-"`
	Author        string         `json:"author"`
	CreatedAt     time.Time      `json:"created_at"`
	UpdatedAt     time.Time      `json:"updated_at"`
}

func (i *Issue) UnmarshalJSON(data []byte) error {
	var raw struct {
		ID        any             `json:"id"`
		Number    any             `json:"number"`
		Title     string          `json:"title"`
		Body      string          `json:"body"`
		Status    string          `json:"status"`
		State     string          `json:"state"`
		Comments  any             `json:"comments"`
		Milestone *Milestone      `json:"milestone"`
		Labels    json.RawMessage `json:"labels"`
		Author    string          `json:"author"`
		CreatedAt time.Time       `json:"created_at"`
		UpdatedAt time.Time       `json:"updated_at"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	var err error
	i.ID, err = decodeID(raw.ID)
	if err != nil {
		return err
	}
	i.Number, err = decodeNumber(raw.Number)
	if err != nil {
		return err
	}
	i.Title = raw.Title
	i.Body = raw.Body
	i.Status = raw.Status
	i.State = raw.State
	i.Comments, err = decodeOptionalInt(raw.Comments)
	if err != nil {
		return err
	}
	i.Milestone = raw.Milestone
	if len(raw.Labels) > 0 {
		if isLabelObjectArray(raw.Labels) {
			if err := json.Unmarshal(raw.Labels, &i.GitCodeLabels); err != nil {
				return err
			}
			i.Labels, err = NormalizeLabels(i.GitCodeLabels)
			if err != nil {
				return err
			}
		} else {
			var strs []string
			if err := json.Unmarshal(raw.Labels, &strs); err != nil {
				return err
			}
			i.Labels = strs
		}
	}
	i.Author = raw.Author
	i.CreatedAt = raw.CreatedAt
	i.UpdatedAt = raw.UpdatedAt
	return nil
}

func isLabelObjectArray(raw json.RawMessage) bool {
	s := strings.TrimSpace(string(raw))
	return len(s) >= 2 && s[0] == '[' && s[1] == '{'
}

func decodeID(value any) (string, error) {
	switch v := value.(type) {
	case string:
		if v == "" || v == "0" {
			return "", &ErrSchemaDecode{Field: "id", Message: "id must not be empty or zero"}
		}
		return v, nil
	case float64:
		if v == 0 {
			return "", &ErrSchemaDecode{Field: "id", Message: "id must not be empty or zero"}
		}
		return strconv.FormatInt(int64(v), 10), nil
	case nil:
		return "", &ErrSchemaDecode{Field: "id", Message: "id is required"}
	default:
		return "", &ErrSchemaDecode{Field: "id", Message: fmt.Sprintf("unexpected id type %T", v)}
	}
}

func decodeNumber(value any) (int, error) {
	switch v := value.(type) {
	case float64:
		return int(v), nil
	case string:
		if v == "" {
			return 0, &ErrSchemaDecode{Field: "number", Message: "number is required"}
		}
		n, err := strconv.Atoi(v)
		if err != nil {
			return 0, &ErrSchemaDecode{Field: "number", Message: fmt.Sprintf("cannot parse number %q: %v", v, err)}
		}
		return n, nil
	case nil:
		return 0, &ErrSchemaDecode{Field: "number", Message: "number is required"}
	default:
		return 0, &ErrSchemaDecode{Field: "number", Message: fmt.Sprintf("unexpected number type %T", v)}
	}
}

func decodeOptionalInt(value any) (int, error) {
	if value == nil {
		return 0, nil
	}
	return decodeNumber(value)
}

type Comment struct {
	ID          string    `json:"id"`
	IssueID     string    `json:"issue_id"`
	IssueNumber int       `json:"issue_number,omitempty"`
	Body        string    `json:"body"`
	Author      string    `json:"author"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

func (c *Comment) UnmarshalJSON(data []byte) error {
	type rawComment struct {
		ID        any             `json:"id"`
		NoteID    any             `json:"note_id"`
		IssueID   any             `json:"issue_id"`
		Body      string          `json:"body"`
		Author    string          `json:"author"`
		User      json.RawMessage `json:"user"`
		CreatedAt string          `json:"created_at"`
		UpdatedAt string          `json:"updated_at"`
		Target    struct {
			Issue struct {
				ID     any `json:"id"`
				Number any `json:"number"`
			} `json:"issue"`
		} `json:"target"`
	}
	var raw rawComment
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	id, err := decodeOptionalID(firstNonNil(raw.NoteID, raw.ID))
	if err != nil {
		return err
	}
	issueID, err := decodeOptionalID(firstNonNil(raw.IssueID, raw.Target.Issue.ID))
	if err != nil {
		return err
	}
	issueNumber, err := decodeOptionalInt(raw.Target.Issue.Number)
	if err != nil {
		return err
	}
	created, err := decodeOptionalTime("comment.created_at", raw.CreatedAt)
	if err != nil {
		return err
	}
	updated, err := decodeOptionalTime("comment.updated_at", raw.UpdatedAt)
	if err != nil {
		return err
	}
	*c = Comment{ID: id, IssueID: issueID, IssueNumber: issueNumber, Body: raw.Body, Author: firstNonEmpty(raw.Author, decodeCommentUser(raw.User)), CreatedAt: created, UpdatedAt: updated}
	return nil
}

func firstNonNil(values ...any) any {
	for _, value := range values {
		if value != nil {
			return value
		}
	}
	return nil
}

func decodeOptionalID(value any) (string, error) {
	if value == nil {
		return "", nil
	}
	return decodeID(value)
}

func decodeOptionalTime(field, value string) (time.Time, error) {
	if strings.TrimSpace(value) == "" {
		return time.Time{}, nil
	}
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		return time.Time{}, &ErrSchemaDecode{Field: field, Expected: "RFC3339 timestamp or absent", Received: fmt.Sprintf("%q", value)}
	}
	return parsed, nil
}

func decodeCommentUser(data json.RawMessage) string {
	if len(data) == 0 || string(data) == "null" {
		return ""
	}
	var user struct {
		Login    string `json:"login"`
		Username string `json:"username"`
		Name     string `json:"name"`
		ID       any    `json:"id"`
	}
	if err := json.Unmarshal(data, &user); err != nil {
		return ""
	}
	id, _ := decodeOptionalID(user.ID)
	return firstNonEmpty(user.Login, user.Username, user.Name, id)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

type PullRequest struct {
	Kind          string         `json:"-"`
	SourceID      string         `json:"-"`
	ID            string         `json:"id"`
	Number        int            `json:"number"`
	HTMLURL       string         `json:"html_url"`
	State         string         `json:"state"`
	Title         string         `json:"title"`
	Body          string         `json:"body"`
	User          string         `json:"-"`
	GitCodeLabels []GitCodeLabel `json:"labels"`
	Labels        []string       `json:"-"`
	Base          string         `json:"-"`
	BaseSHA       string         `json:"-"`
	Head          string         `json:"-"`
	HeadSHA       string         `json:"-"`
	CreatedAt     time.Time      `json:"created_at"`
	UpdatedAt     time.Time      `json:"updated_at"`
}

func (p *PullRequest) UnmarshalJSON(data []byte) error {
	var raw struct {
		ID        any             `json:"id"`
		Number    any             `json:"number"`
		HTMLURL   string          `json:"html_url"`
		State     string          `json:"state"`
		Title     string          `json:"title"`
		Body      string          `json:"body"`
		User      json.RawMessage `json:"user"`
		Labels    json.RawMessage `json:"labels"`
		Base      json.RawMessage `json:"base"`
		Head      json.RawMessage `json:"head"`
		CreatedAt time.Time       `json:"created_at"`
		UpdatedAt time.Time       `json:"updated_at"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	id, err := decodeID(raw.ID)
	if err != nil {
		return err
	}
	number, err := decodeNumber(raw.Number)
	if err != nil {
		return err
	}
	p.Kind = "pull_request"
	p.SourceID = pullRequestSourceID(number)
	p.ID = id
	p.Number = number
	p.HTMLURL = raw.HTMLURL
	p.State = raw.State
	p.Title = raw.Title
	p.Body = raw.Body
	p.User = decodeActor(raw.User)
	if len(raw.Labels) > 0 {
		if isLabelObjectArray(raw.Labels) {
			if err := json.Unmarshal(raw.Labels, &p.GitCodeLabels); err != nil {
				return err
			}
			p.Labels, err = NormalizeLabels(p.GitCodeLabels)
			if err != nil {
				return err
			}
		} else {
			var strs []string
			if err := json.Unmarshal(raw.Labels, &strs); err != nil {
				return err
			}
			p.Labels = strs
		}
	}
	p.Base = decodeRef(raw.Base)
	p.BaseSHA = decodeRefSHA(raw.Base)
	p.Head = decodeRef(raw.Head)
	p.HeadSHA = decodeRefSHA(raw.Head)
	p.CreatedAt = raw.CreatedAt
	p.UpdatedAt = raw.UpdatedAt
	return nil
}

type PRComment struct {
	Kind             string              `json:"-"`
	ID               string              `json:"id"`
	Body             string              `json:"body"`
	Author           string              `json:"author"`
	DiscussionID     string              `json:"discussion_id"`
	ReviewKind       string              `json:"review_kind,omitempty"`
	Path             string              `json:"path,omitempty"`
	Line             int                 `json:"line,omitempty"`
	StartLine        int                 `json:"start_line,omitempty"`
	EndLine          int                 `json:"end_line,omitempty"`
	Position         int                 `json:"position,omitempty"`
	OriginalPosition int                 `json:"original_position,omitempty"`
	Resolved         *bool               `json:"resolved,omitempty"`
	Resolvable       *bool               `json:"resolvable,omitempty"`
	ParentID         string              `json:"parent_id,omitempty"`
	Replies          []PRComment         `json:"-"`
	Thread           []PRComment         `json:"-"`
	Positions        []PRCommentPosition `json:"positions,omitempty"`
	PRNumber         int                 `json:"-"`
	CreatedAt        time.Time           `json:"created_at"`
	UpdatedAt        time.Time           `json:"updated_at"`
}

type PRCommentPosition struct {
	PositionKind  string `json:"position_kind,omitempty"`
	PositionType  string `json:"position_type,omitempty"`
	BaseSHA       string `json:"base_sha,omitempty"`
	StartSHA      string `json:"start_sha,omitempty"`
	HeadSHA       string `json:"head_sha,omitempty"`
	OldPath       string `json:"old_path,omitempty"`
	NewPath       string `json:"new_path,omitempty"`
	OldLine       int    `json:"old_line,omitempty"`
	NewLine       int    `json:"new_line,omitempty"`
	StartOldLine  int    `json:"start_old_line,omitempty"`
	StartNewLine  int    `json:"start_new_line,omitempty"`
	LineCode      string `json:"line_code,omitempty"`
	StartLineCode string `json:"start_line_code,omitempty"`
	PatchsetIID   int    `json:"patchset_iid,omitempty"`
	DiffID        int    `json:"diff_id,omitempty"`
	VersionSHA    string `json:"version_sha,omitempty"`
	Side          string `json:"side,omitempty"`
	IsOutdated    *bool  `json:"is_outdated,omitempty"`
}

func (c *PRComment) UnmarshalJSON(data []byte) error {
	var raw struct {
		ID               any             `json:"id"`
		NoteID           any             `json:"note_id"`
		Body             string          `json:"body"`
		Author           string          `json:"author"`
		User             json.RawMessage `json:"user"`
		DiscussionID     any             `json:"discussion_id"`
		Type             string          `json:"type"`
		Kind             string          `json:"kind"`
		NoteableType     string          `json:"noteable_type"`
		Path             string          `json:"path"`
		FilePath         string          `json:"file_path"`
		NewPath          string          `json:"new_path"`
		Line             any             `json:"line"`
		NewLine          any             `json:"new_line"`
		StartLine        any             `json:"start_line"`
		EndLine          any             `json:"end_line"`
		Position         json.RawMessage `json:"position"`
		OriginalPosition any             `json:"original_position"`
		Resolved         *bool           `json:"resolved"`
		Resolvable       *bool           `json:"resolvable"`
		ParentID         any             `json:"parent_id"`
		InReplyToID      any             `json:"in_reply_to_id"`
		ReplyID          any             `json:"reply_id"`
		Replies          []PRComment     `json:"reply"`
		CreatedAt        string          `json:"created_at"`
		UpdatedAt        string          `json:"updated_at"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	id, err := decodeOptionalID(firstNonNil(raw.NoteID, raw.ID))
	if err != nil {
		return err
	}
	discussionID, err := decodeOptionalID(raw.DiscussionID)
	if err != nil {
		return err
	}
	created, err := decodeOptionalTime("pr_comment.created_at", raw.CreatedAt)
	if err != nil {
		return err
	}
	updated, err := decodeOptionalTime("pr_comment.updated_at", raw.UpdatedAt)
	if err != nil {
		return err
	}
	line, err := decodeOptionalInt(firstNonNil(raw.Line, raw.NewLine))
	if err != nil {
		return err
	}
	startLine, err := decodeOptionalInt(raw.StartLine)
	if err != nil {
		return err
	}
	endLine, err := decodeOptionalInt(raw.EndLine)
	if err != nil {
		return err
	}
	position, positions, positionPath, positionLine, err := decodePRCommentPosition(raw.Position)
	if err != nil {
		return err
	}
	originalPosition, err := decodeOptionalInt(raw.OriginalPosition)
	if err != nil {
		return err
	}
	parentID, err := decodeOptionalID(firstNonNil(raw.ParentID, raw.InReplyToID, raw.ReplyID))
	if err != nil {
		return err
	}
	path := firstNonEmpty(raw.Path, raw.FilePath, raw.NewPath, positionPath)
	if line == 0 {
		line = positionLine
	}
	reviewKind := "general"
	rawKind := strings.ToLower(firstNonEmpty(raw.Kind, raw.Type, raw.NoteableType))
	if path != "" || line > 0 || position > 0 || strings.Contains(rawKind, "inline") || strings.Contains(rawKind, "diff") {
		reviewKind = "inline"
	}
	*c = PRComment{Kind: "pr_comment", ID: id, Body: raw.Body, Author: firstNonEmpty(raw.Author, decodeActor(raw.User)), DiscussionID: discussionID, ReviewKind: reviewKind, Path: path, Line: line, StartLine: startLine, EndLine: endLine, Position: position, OriginalPosition: originalPosition, Resolved: raw.Resolved, Resolvable: raw.Resolvable, ParentID: parentID, Replies: raw.Replies, Positions: positions, CreatedAt: created, UpdatedAt: updated}
	return nil
}

func decodePRCommentPosition(raw json.RawMessage) (int, []PRCommentPosition, string, int, error) {
	if len(raw) == 0 || string(raw) == "null" {
		return 0, nil, "", 0, nil
	}
	var scalar any
	if err := json.Unmarshal(raw, &scalar); err != nil {
		return 0, nil, "", 0, err
	}
	if _, ok := scalar.(map[string]any); !ok {
		position, err := decodeOptionalInt(scalar)
		return position, nil, "", 0, err
	}
	var diff prDiffPosition
	if err := json.Unmarshal(raw, &diff); err != nil {
		return 0, nil, "", 0, err
	}
	position, err := prDiffPositionToCommentPosition("current", &diff, nil)
	if err != nil {
		return 0, nil, "", 0, err
	}
	line := firstPositive(position.NewLine, position.OldLine)
	return 0, []PRCommentPosition{position}, firstNonEmpty(position.NewPath, position.OldPath), line, nil
}

func pullRequestSourceID(number int) string {
	return "PR-" + strconv.Itoa(number)
}

func decodeActor(data json.RawMessage) string {
	return decodeCommentUser(data)
}

func decodeRef(data json.RawMessage) string {
	if len(data) == 0 || string(data) == "null" {
		return ""
	}
	var ref struct {
		Ref   string `json:"ref"`
		Label string `json:"label"`
		Sha   string `json:"sha"`
		Repo  struct {
			FullName string `json:"full_name"`
		} `json:"repo"`
	}
	if err := json.Unmarshal(data, &ref); err != nil {
		return ""
	}
	return firstNonEmpty(ref.Ref, ref.Label, ref.Sha, ref.Repo.FullName)
}

func decodeRefSHA(data json.RawMessage) string {
	if len(data) == 0 || string(data) == "null" {
		return ""
	}
	var ref struct {
		Sha string `json:"sha"`
	}
	if err := json.Unmarshal(data, &ref); err != nil {
		return ""
	}
	return ref.Sha
}

type WikiPage struct {
	ID        string    `json:"id"`
	Slug      string    `json:"slug"`
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	Revision  string    `json:"revision"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

type SearchResult struct {
	ID        string    `json:"id"`
	Type      string    `json:"type"`
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	URL       string    `json:"url"`
	Score     float64   `json:"score"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

type AttachmentSummary struct {
	ID          string    `json:"id"`
	Name        string    `json:"name"`
	ContentType string    `json:"content_type"`
	Size        int64     `json:"size"`
	Checksum    string    `json:"checksum"`
	DownloadURL string    `json:"download_url"`
	CreatedAt   time.Time `json:"created_at"`
}

type AttachmentBody struct {
	ID             string
	Name           string
	ContentType    string
	Size           int64
	Body           []byte
	SourceEndpoint string
	Checksum       string
}

type WriteOptions struct {
	IdempotencyKey   string
	IdempotencyNonce string

	// BeforeIssueUpdateMutation runs after the canonical issue preimage has
	// been read and immediately before the PATCH is attempted. The service
	// uses this hook to durably claim and fence the mutation.
	BeforeIssueUpdateMutation func(Issue) error

	// BeforePRUpdateMutation runs after the canonical pull request preimage has
	// been read and immediately before the PATCH is attempted. The service
	// uses this hook to durably claim and fence the mutation.
	BeforePRUpdateMutation func(PullRequest) error

	// BeforeMergePRMutation runs after the canonical pull request preimage and
	// optional head-SHA guard have been checked. It runs before either returning
	// an already-merged confirmation or attempting the merge PUT. The service
	// uses this hook to durably claim settlement and fence any mutation.
	BeforeMergePRMutation func(PullRequest) error

	// Mutation transport controls are intentionally package-private. They let
	// guarded writes distinguish a proven pre-request failure from an ambiguous
	// request and prevent the generic read retry policy from replaying a write.
	singleTransportAttempt bool
	beforeTransportAttempt func()
}

type WriteResult[T any] struct {
	Record                     T
	Confirmed                  bool
	Operation                  string
	Target                     string
	ProviderStatus             string
	RemoteID                   string
	RemoteNumber               int
	RemoteSlug                 string
	RemoteRevision             string
	APIPath                    string
	CachePath                  string
	BrowserURL                 string
	ParentIssueNumber          int
	ParentIssueID              string
	IdempotencyKey             string
	ResponseHash               string
	ConfirmedAt                time.Time
	ProviderPayloadFingerprint string
}

type CreateIssueRequest struct {
	Owner     string          `json:"-"`
	Repo      string          `json:"-"`
	Title     string          `json:"title"`
	Body      string          `json:"body,omitempty"`
	Labels    json.RawMessage `json:"labels,omitempty"`
	Milestone json.RawMessage `json:"milestone,omitempty"`
}

type UpdateIssueRequest struct {
	Owner     string          `json:"-"`
	Repo      string          `json:"-"`
	Number    int             `json:"-"`
	Title     string          `json:"title,omitempty"`
	Body      string          `json:"body,omitempty"`
	State     string          `json:"state,omitempty"`
	Labels    json.RawMessage `json:"labels,omitempty"`
	Milestone json.RawMessage `json:"milestone,omitempty"`
}

type CreateIssueCommentRequest struct {
	Owner  string `json:"-"`
	Repo   string `json:"-"`
	Number int    `json:"-"`
	Body   string `json:"body"`
}

type UpdateIssueCommentRequest struct {
	Owner     string `json:"-"`
	Repo      string `json:"-"`
	Number    int    `json:"-"`
	CommentID string `json:"-"`
	Body      string `json:"body"`
}

type WikiContentsEntry struct {
	Path string `json:"path"`
	Type string `json:"type"`
	Sha  string `json:"sha,omitempty"`
	Size int64  `json:"size,omitempty"`
}

type WikiContentsFile struct {
	Path     string `json:"path"`
	Type     string `json:"type"`
	Sha      string `json:"sha"`
	Content  string `json:"content,omitempty"`
	Encoding string `json:"encoding,omitempty"`
	Size     int64  `json:"size,omitempty"`
}

type WikiContentWriteRequest struct {
	Content string `json:"content,omitempty"`
	Message string `json:"message,omitempty"`
	Sha     string `json:"sha,omitempty"`
}

type CreateWikiPageRequest struct {
	Owner   string `json:"-"`
	Repo    string `json:"-"`
	Slug    string `json:"-"`
	Path    string `json:"-"`
	Title   string `json:"-"`
	Body    string `json:"-"`
	Message string `json:"-"`
}

type UpdateWikiPageRequest struct {
	Owner   string `json:"-"`
	Repo    string `json:"-"`
	Slug    string `json:"-"`
	Path    string `json:"-"`
	Title   string `json:"-"`
	Body    string `json:"-"`
	Sha     string `json:"-"`
	Message string `json:"-"`
}

type DeleteWikiPageRequest struct {
	Owner   string `json:"-"`
	Repo    string `json:"-"`
	Slug    string `json:"-"`
	Path    string `json:"-"`
	Sha     string `json:"-"`
	Message string `json:"-"`
}

type LabelRequest struct {
	Owner  string `json:"-"`
	Repo   string `json:"-"`
	Number int    `json:"-"`
	Label  string `json:"label"`
}

type MilestoneListRequest struct {
	Owner   string
	Repo    string
	State   string
	Page    int
	PerPage int
}

type MilestoneRequest struct {
	Owner string
	Repo  string
	ID    int
}

type MilestoneWriteRequest struct {
	Owner       string
	Repo        string
	ID          int
	Title       string
	Description string
	DueOn       string
	State       string
}

type Milestone struct {
	RemoteID  string `json:"-"`
	SourceID  string `json:"-"`
	Title     string `json:"-"`
	Body      string `json:"-"`
	Status    string `json:"-"`
	DueOn     string `json:"-"`
	HTMLURL   string `json:"-"`
	CreatedAt string `json:"-"`
	UpdatedAt string `json:"-"`
}

func (m *Milestone) UnmarshalJSON(data []byte) error {
	var raw struct {
		ID          any    `json:"id"`
		Number      any    `json:"number"`
		Title       string `json:"title"`
		Name        string `json:"name"`
		Description string `json:"description"`
		Body        string `json:"body"`
		State       string `json:"state"`
		Status      string `json:"status"`
		DueOn       string `json:"due_on"`
		DueDate     string `json:"due_date"`
		HTMLURL     string `json:"html_url"`
		URL         string `json:"url"`
		CreatedAt   string `json:"created_at"`
		UpdatedAt   string `json:"updated_at"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	var err error
	identity := raw.ID
	if milestoneIdentityMissing(identity) {
		identity = raw.Number
	}
	m.RemoteID, err = decodeMilestoneID(identity)
	if err != nil {
		m.RemoteID = ""
		return err
	}
	if strings.TrimSpace(raw.Title) == "" {
		if strings.TrimSpace(raw.Name) != "" {
			return &ErrSchemaDecode{Field: "milestone.title", Expected: "non-empty string", Received: "missing", Message: "title is required; name alone is not accepted"}
		}
		return &ErrSchemaDecode{Field: "milestone.title", Expected: "non-empty string", Received: "missing"}
	}
	m.SourceID = "MILESTONE-" + m.RemoteID
	m.Title = raw.Title
	if strings.TrimSpace(raw.Description) != "" {
		m.Body = raw.Description
	} else {
		m.Body = raw.Body
	}
	switch r := any(raw.Description).(type) {
	case string:
		break
	case nil:
		break
	default:
		return &ErrSchemaDecode{Field: "milestone.description", Expected: "string or absent", Received: fmt.Sprintf("type %T", r)}
	}
	switch r := any(raw.Body).(type) {
	case string:
		break
	case nil:
		break
	default:
		return &ErrSchemaDecode{Field: "milestone.body", Expected: "string or absent", Received: fmt.Sprintf("type %T", r)}
	}
	m.Status, err = decodeMilestoneStatus(raw.State, raw.Status)
	if err != nil {
		return err
	}
	m.DueOn, err = decodeMilestoneDate(raw.DueOn, raw.DueDate)
	if err != nil {
		return err
	}
	if raw.CreatedAt != "" {
		if !isValidTimestamp(raw.CreatedAt) {
			return &ErrSchemaDecode{Field: "milestone.created_at", Expected: "RFC3339 timestamp or absent", Received: fmt.Sprintf("%q", raw.CreatedAt)}
		}
		m.CreatedAt = raw.CreatedAt
	}
	if raw.UpdatedAt != "" {
		if !isValidTimestamp(raw.UpdatedAt) {
			return &ErrSchemaDecode{Field: "milestone.updated_at", Expected: "RFC3339 timestamp or absent", Received: fmt.Sprintf("%q", raw.UpdatedAt)}
		}
		m.UpdatedAt = raw.UpdatedAt
	}
	m.HTMLURL = firstNonEmpty(raw.HTMLURL, raw.URL)
	return nil
}

func milestoneIdentityMissing(value any) bool {
	if value == nil {
		return true
	}
	if text, ok := value.(string); ok {
		return strings.TrimSpace(text) == ""
	}
	return false
}

func decodeMilestoneID(value any) (string, error) {
	switch v := value.(type) {
	case string:
		if v == "" {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "non-empty decimal string", Received: "empty string"}
		}
		if _, err := strconv.Atoi(v); err != nil {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "decimal integer string", Received: fmt.Sprintf("%q", v)}
		}
		// reject "0" and negative
		n, _ := strconv.Atoi(v)
		if n == 0 {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "positive integer", Received: fmt.Sprintf("%q", v)}
		}
		return v, nil
	case float64:
		if v == 0 {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "non-empty positive integer", Received: "zero"}
		}
		if v != float64(int64(v)) {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "integer value", Received: fmt.Sprintf("fractional %.1f", v)}
		}
		if v < 0 {
			return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "positive integer", Received: fmt.Sprintf("%.0f", v)}
		}
		return strconv.FormatInt(int64(v), 10), nil
	case nil:
		return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "non-empty positive integer", Received: "nil"}
	default:
		return "", &ErrSchemaDecode{Field: "milestone.id", Expected: "number or string", Received: fmt.Sprintf("type %T", v)}
	}
}

func decodeMilestoneStatus(state, status string) (string, error) {
	val := strings.ToLower(strings.TrimSpace(state))
	if val == "" {
		val = strings.ToLower(strings.TrimSpace(status))
	}
	switch val {
	case "open", "active":
		return "open", nil
	case "closed":
		return "closed", nil
	case "":
		return "open", nil
	default:
		field := "milestone.state"
		if strings.TrimSpace(state) == "" {
			field = "milestone.status"
		}
		return "", &ErrSchemaDecode{
			Field:    field,
			Expected: "open, active, closed, or absent",
			Received: fmt.Sprintf("%q", val),
		}
	}
}

func decodeMilestoneDate(dueOn, dueDate string) (string, error) {
	val := strings.TrimSpace(dueOn)
	if val == "" {
		val = strings.TrimSpace(dueDate)
	}
	if val == "" {
		return "", nil
	}
	if t, err := time.Parse(time.RFC3339, val); err == nil {
		return t.Format("2006-01-02"), nil
	}
	if t, err := time.Parse("2006-01-02", val); err == nil {
		return t.Format("2006-01-02"), nil
	}
	return "", &ErrSchemaDecode{Field: "milestone.due_on", Expected: "YYYY-MM-DD or RFC3339", Received: fmt.Sprintf("%q", val)}
}

func isValidTimestamp(val string) bool {
	_, err := time.Parse(time.RFC3339, val)
	return err == nil
}

func milestoneListQuery(req MilestoneListRequest) url.Values {
	values := url.Values{}
	if req.State != "" {
		values.Set("state", req.State)
	}
	return values
}

func prListQuery(req PRListRequest) url.Values {
	values := url.Values{}
	if req.State != "" {
		values.Set("state", req.State)
	}
	if req.OrderBy != "" {
		values.Set("order_by", req.OrderBy)
	}
	if req.Direction != "" {
		values.Set("direction", req.Direction)
	}
	return values
}