package knowledgeqa

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/knowledgeqa/protocol"
)

const (
	knowledgeQAShareIDPrefix    = "kqh"
	knowledgeQAShareTokenPrefix = "kqshare_"
	knowledgeQAShareTokenBytes  = 32
)

// Share is an immutable answer snapshot crossing the source workspace
// boundary. Citation destinations are replaced with presentation-only evidence
// markers before persistence, so the recipient never receives an owner-scoped
// repository capability.
type Share struct {
	ID                   string
	SourceWorkspaceID    string
	SourceOrganizationID string
	SourceSessionID      string
	SourceTurnID         string
	OwnerUserID          string
	OwnerDisplayName     string
	Question             string
	Answer               string
	Token                string
	CreatedAt            time.Time
	UpdatedAt            time.Time
	LastOpenedAt         time.Time
	Citations            []ShareCitation
}

type ShareCitation struct {
	ShareID              string
	EvidenceID           string
	SourceWorkspaceID    string
	SourceOrganizationID string
	SourceTurnID         string
	Citation             Citation
}

// CreateShare rotates the invitation token for one completed answer while
// retaining recipients that already accepted an earlier token.
func (store *Store) CreateShare(
	ctx context.Context,
	workspaceID string,
	ownerUserID string,
	sessionID string,
	turnID string,
	now time.Time,
) (Share, error) {
	if store == nil || store.db == nil {
		return Share{}, fmt.Errorf("knowledge Q&A store is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	sessionID = strings.TrimSpace(sessionID)
	turnID = strings.TrimSpace(turnID)
	now = normalizeNow(now)
	if workspaceID == "" || ownerUserID == "" || sessionID == "" || turnID == "" {
		return Share{}, ErrNotFound
	}
	tokenSuffix, err := secrets.NewToken(knowledgeQAShareTokenBytes)
	if err != nil {
		return Share{}, fmt.Errorf("generate Knowledge Q&A share token: %w", err)
	}
	token := knowledgeQAShareTokenPrefix + tokenSuffix
	tokenHash := sha256.Sum256([]byte(token))
	generatedID, err := identifiers.NewEntityID(knowledgeQAShareIDPrefix)
	if err != nil {
		return Share{}, fmt.Errorf("generate Knowledge Q&A share id: %w", err)
	}

	var result Share
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		snapshot, err := loadCompletedShareSnapshot(
			ctx, tx, workspaceID, ownerUserID, sessionID, turnID,
		)
		if err != nil {
			return err
		}
		result, err = persistKnowledgeQAShare(
			ctx, tx, snapshot, generatedID, workspaceID, ownerUserID, turnID,
			tokenHash, now,
		)
		if err != nil {
			return err
		}
		return replaceKnowledgeQAShareCitations(ctx, tx, result, now)
	})
	if err != nil {
		return Share{}, err
	}
	result.Token = token
	return result, nil
}

func loadCompletedShareSnapshot(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	ownerUserID string,
	sessionID string,
	turnID string,
) (Share, error) {
	var result Share
	var answer string
	if err := tx.QueryRow(ctx, `
SELECT turn.organization_id, session.id, turn.question, message.content,
       COALESCE(NULLIF(TRIM(owner.display_name),''), NULLIF(TRIM(owner.name),''), '')
FROM knowledge_qa_turns AS turn
JOIN knowledge_qa_sessions AS session
  ON session.id=turn.session_id AND session.workspace_id=turn.workspace_id
JOIN scope_messages AS message
  ON message.workspace_id=turn.workspace_id
 AND message.scope_id=session.thread_channel_id
 AND message.id=turn.assistant_message_id
JOIN users AS owner ON owner.id=session.owner_user_id
WHERE turn.workspace_id=$1 AND session.owner_user_id=$2
  AND session.id=$3 AND turn.id=$4 AND turn.status='completed'
  AND turn.assistant_message_id<>''
FOR UPDATE
`, workspaceID, ownerUserID, sessionID, turnID).Scan(
		&result.SourceOrganizationID,
		&result.SourceSessionID,
		&result.Question,
		&answer,
		&result.OwnerDisplayName,
	); err != nil {
		if coredb.IsNotFound(err) {
			return Share{}, ErrNotFound
		}
		return Share{}, fmt.Errorf("authorize Knowledge Q&A share: %w", err)
	}
	result.OwnerDisplayName = strings.TrimSpace(result.OwnerDisplayName)
	result.Question = protocol.StripCitationDestinations(
		strings.TrimSpace(result.Question),
	)
	preparedAnswer, references, err := protocol.PrepareSharedEvidenceMarkdown(
		strings.TrimSpace(answer),
	)
	if err != nil {
		return Share{}, ErrInvalidState
	}
	result.Answer = preparedAnswer
	if result.Question == "" || strings.TrimSpace(result.Answer) == "" {
		return Share{}, ErrInvalidState
	}
	result.Citations, err = loadCompletedShareCitations(
		ctx, tx, workspaceID, turnID, references,
	)
	if err != nil {
		return Share{}, err
	}
	return result, nil
}

func loadCompletedShareCitations(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	turnID string,
	references []protocol.SharedEvidenceReference,
) ([]ShareCitation, error) {
	if len(references) == 0 {
		return nil, nil
	}
	placeholders := make([]string, len(references))
	args := make([]any, 0, len(references)+2)
	args = append(args, workspaceID, turnID)
	for index, reference := range references {
		placeholders[index] = fmt.Sprintf("$%d", index+3)
		args = append(args, reference.CitationID)
	}
	rows, err := tx.Query(ctx, `
SELECT citation.id, citation.workspace_id, citation.turn_id,
       citation.attempt_id, citation.citation_kind, citation.knowledge_base_id,
       COALESCE(citation.source_id,''), citation.root_kind, citation.repo_role,
       citation.revision, citation.resolved_ref, citation.resolved_ref_kind,
       citation.path, citation.line_start, citation.line_end,
       COALESCE(citation.url,''), COALESCE(citation.title,''),
       COALESCE(citation.snippet,''), citation.fetched_at, citation.created_at
FROM knowledge_qa_citations AS citation
JOIN knowledge_qa_turns AS turn
  ON turn.id=citation.turn_id AND turn.workspace_id=citation.workspace_id
WHERE citation.workspace_id=$1 AND citation.turn_id=$2
  AND citation.attempt_id=turn.current_attempt_id
  AND citation.id IN (`+strings.Join(placeholders, ",")+`)
`, args...)
	if err != nil {
		return nil, fmt.Errorf("load Knowledge Q&A share citations: %w", err)
	}
	defer rows.Close()
	byID := make(map[string]Citation, len(references))
	for rows.Next() {
		var citation Citation
		if err := rows.Scan(
			&citation.ID, &citation.WorkspaceID, &citation.TurnID,
			&citation.AttemptID, &citation.CitationKind,
			&citation.KnowledgeBaseID, &citation.SourceID,
			&citation.RootKind, &citation.RepoRole, &citation.Revision,
			&citation.ResolvedRef, &citation.ResolvedRefKind, &citation.Path,
			&citation.LineStart, &citation.LineEnd, &citation.URL,
			&citation.Title, &citation.Snippet, &citation.FetchedAt,
			&citation.CreatedAt,
		); err != nil {
			return nil, fmt.Errorf("scan Knowledge Q&A share citation: %w", err)
		}
		byID[citation.ID] = citation
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate Knowledge Q&A share citations: %w", err)
	}
	result := make([]ShareCitation, 0, len(references))
	for _, reference := range references {
		citation, ok := byID[reference.CitationID]
		if !ok {
			return nil, ErrInvalidState
		}
		result = append(result, ShareCitation{
			EvidenceID: reference.EvidenceID,
			Citation:   citation,
		})
	}
	return result, nil
}

func persistKnowledgeQAShare(
	ctx context.Context,
	tx coredb.Tx,
	result Share,
	generatedID string,
	workspaceID string,
	ownerUserID string,
	turnID string,
	tokenHash [sha256.Size]byte,
	now time.Time,
) (Share, error) {
	var existingID string
	var createdAt time.Time
	err := tx.QueryRow(ctx, `
SELECT id, created_at
FROM knowledge_qa_shares
WHERE source_turn_id=$1
FOR UPDATE
`, turnID).Scan(&existingID, &createdAt)
	switch {
	case err == nil:
		result.ID = existingID
		result.CreatedAt = createdAt
		updateResult, err := tx.Exec(ctx, `
UPDATE knowledge_qa_shares
SET token_hash=$1, owner_display_name=$2, question=$3, answer=$4,
    updated_at=$5
WHERE id=$6 AND source_workspace_id=$7 AND owner_user_id=$8
`, tokenHash[:], result.OwnerDisplayName, result.Question, result.Answer,
			now, result.ID, workspaceID, ownerUserID)
		if err != nil {
			return Share{}, fmt.Errorf("rotate Knowledge Q&A share token: %w", err)
		}
		if updateResult.RowsAffected() != 1 {
			return Share{}, ErrNotFound
		}
	case coredb.IsNotFound(err):
		result.ID = generatedID
		result.CreatedAt = now
		if _, err := tx.Exec(ctx, `
INSERT INTO knowledge_qa_shares (
  id, token_hash, source_workspace_id, source_organization_id,
  source_session_id, source_turn_id, owner_user_id, owner_display_name,
  question, answer, created_at, updated_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$11)
`, result.ID, tokenHash[:], workspaceID, result.SourceOrganizationID,
			result.SourceSessionID, turnID, ownerUserID, result.OwnerDisplayName,
			result.Question, result.Answer, now); err != nil {
			return Share{}, fmt.Errorf("create Knowledge Q&A share: %w", err)
		}
	default:
		return Share{}, fmt.Errorf("load existing Knowledge Q&A share: %w", err)
	}
	result.SourceWorkspaceID = workspaceID
	result.SourceTurnID = turnID
	result.OwnerUserID = ownerUserID
	result.UpdatedAt = now
	return result, nil
}

func replaceKnowledgeQAShareCitations(
	ctx context.Context,
	tx coredb.Tx,
	share Share,
	now time.Time,
) error {
	if _, err := tx.Exec(ctx, `DELETE FROM knowledge_qa_share_citations WHERE share_id=$1`, share.ID); err != nil {
		return fmt.Errorf("replace Knowledge Q&A share citations: %w", err)
	}
	if len(share.Citations) == 0 {
		return nil
	}
	const fieldCount = 13
	values := make([]string, 0, len(share.Citations))
	args := make([]any, 0, len(share.Citations)*fieldCount)
	for _, shared := range share.Citations {
		citation := shared.Citation
		offset := len(args)
		values = append(values, fmt.Sprintf(
			"($%d,$%d,$%d,NULLIF($%d,''),$%d,$%d,$%d,$%d,$%d,$%d,$%d,$%d,$%d)",
			offset+1, offset+2, offset+3, offset+4, offset+5, offset+6,
			offset+7, offset+8, offset+9, offset+10, offset+11, offset+12,
			offset+13,
		))
		args = append(args,
			share.ID, shared.EvidenceID, citation.KnowledgeBaseID,
			citation.SourceID, citation.RootKind,
			citation.RepoRole, citation.Revision, citation.ResolvedRef,
			citation.ResolvedRefKind, citation.Path, citation.LineStart,
			citation.LineEnd, now,
		)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO knowledge_qa_share_citations (
  share_id, evidence_id, knowledge_base_id, source_id,
  root_kind, repo_role, revision, resolved_ref, resolved_ref_kind, path,
  line_start, line_end, created_at
) VALUES `+strings.Join(values, ","), args...); err != nil {
		return fmt.Errorf("persist Knowledge Q&A share citations: %w", err)
	}
	return nil
}

// OpenShare accepts a high-entropy invitation token for an authenticated Human
// and attaches the immutable answer to that Human's "Shared with me" list.
func (store *Store) OpenShare(
	ctx context.Context,
	recipientUserID string,
	token string,
	now time.Time,
) (Share, error) {
	if store == nil || store.db == nil {
		return Share{}, fmt.Errorf("knowledge Q&A store is not configured")
	}
	recipientUserID = strings.TrimSpace(recipientUserID)
	token = strings.TrimSpace(token)
	now = normalizeNow(now)
	if recipientUserID == "" || !validKnowledgeQAShareToken(token) {
		return Share{}, ErrNotFound
	}
	tokenHash := sha256.Sum256([]byte(token))
	var result Share
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		share, err := scanShare(tx.QueryRow(ctx, shareSelectSQL("share.created_at")+`
WHERE share.token_hash=$1
FOR UPDATE
`, tokenHash[:]))
		if err != nil {
			return err
		}
		if _, err := tx.Exec(ctx, `
INSERT INTO knowledge_qa_share_recipients (
  share_id, recipient_user_id, first_opened_at, last_opened_at
) VALUES ($1,$2,$3,$3)
ON DUPLICATE KEY UPDATE last_opened_at=VALUES(last_opened_at)
`, share.ID, recipientUserID, now); err != nil {
			return fmt.Errorf("attach Knowledge Q&A share recipient: %w", err)
		}
		share.LastOpenedAt = now
		result = share
		return nil
	})
	if err != nil {
		return Share{}, err
	}
	return result, nil
}

// LoadReceivedShare requires a prior successful token open by this recipient.
func (store *Store) LoadReceivedShare(
	ctx context.Context,
	recipientUserID string,
	shareID string,
	now time.Time,
) (Share, error) {
	if store == nil || store.db == nil {
		return Share{}, fmt.Errorf("knowledge Q&A store is not configured")
	}
	recipientUserID = strings.TrimSpace(recipientUserID)
	shareID = strings.TrimSpace(shareID)
	now = normalizeNow(now)
	if recipientUserID == "" || shareID == "" {
		return Share{}, ErrNotFound
	}
	var result Share
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		share, err := scanShare(tx.QueryRow(ctx, shareSelectSQL("recipient.last_opened_at")+`
JOIN knowledge_qa_share_recipients AS recipient ON recipient.share_id=share.id
WHERE share.id=$1 AND recipient.recipient_user_id=$2
FOR UPDATE
`, shareID, recipientUserID))
		if err != nil {
			return err
		}
		if _, err := tx.Exec(ctx, `
UPDATE knowledge_qa_share_recipients
SET last_opened_at=$1
WHERE share_id=$2 AND recipient_user_id=$3
`, now, shareID, recipientUserID); err != nil {
			return fmt.Errorf("touch received Knowledge Q&A share: %w", err)
		}
		share.LastOpenedAt = now
		result = share
		return nil
	})
	if err != nil {
		return Share{}, err
	}
	return result, nil
}

// LoadReceivedShareCitation resolves only a citation snapshot explicitly
// attached to an account-bound received share. Source workspace and turn facts
// are returned server-side for the live permission recheck, never in share DTOs.
func (store *Store) LoadReceivedShareCitation(
	ctx context.Context,
	recipientUserID string,
	shareID string,
	evidenceID string,
) (ShareCitation, error) {
	if store == nil || store.db == nil {
		return ShareCitation{}, fmt.Errorf("knowledge Q&A store is not configured")
	}
	recipientUserID = strings.TrimSpace(recipientUserID)
	shareID = strings.TrimSpace(shareID)
	evidenceID = strings.TrimSpace(evidenceID)
	if recipientUserID == "" || shareID == "" || evidenceID == "" {
		return ShareCitation{}, ErrNotFound
	}
	var result ShareCitation
	result.ShareID = shareID
	result.EvidenceID = evidenceID
	result.Citation.CitationKind = "knowledge"
	err := store.db.QueryRow(ctx, `
SELECT share.source_workspace_id, share.source_organization_id,
       share.source_turn_id, citation.knowledge_base_id,
       COALESCE(citation.source_id,''),
       citation.root_kind, citation.repo_role, citation.revision,
       citation.resolved_ref, citation.resolved_ref_kind, citation.path,
       citation.line_start, citation.line_end
FROM knowledge_qa_shares AS share
JOIN knowledge_qa_share_recipients AS recipient ON recipient.share_id=share.id
JOIN knowledge_qa_share_citations AS citation ON citation.share_id=share.id
WHERE share.id=$1 AND recipient.recipient_user_id=$2
  AND citation.evidence_id=$3
`, shareID, recipientUserID, evidenceID).Scan(
		&result.SourceWorkspaceID, &result.SourceOrganizationID,
		&result.SourceTurnID, &result.Citation.KnowledgeBaseID,
		&result.Citation.SourceID,
		&result.Citation.RootKind, &result.Citation.RepoRole,
		&result.Citation.Revision, &result.Citation.ResolvedRef,
		&result.Citation.ResolvedRefKind, &result.Citation.Path,
		&result.Citation.LineStart, &result.Citation.LineEnd,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return ShareCitation{}, ErrNotFound
		}
		return ShareCitation{}, fmt.Errorf("load received Knowledge Q&A share citation: %w", err)
	}
	return result, nil
}

func (store *Store) ListReceivedShares(
	ctx context.Context,
	recipientUserID string,
	limit int,
) ([]Share, error) {
	if store == nil || store.db == nil {
		return nil, fmt.Errorf("knowledge Q&A store is not configured")
	}
	recipientUserID = strings.TrimSpace(recipientUserID)
	if recipientUserID == "" {
		return nil, ErrNotFound
	}
	if limit <= 0 || limit > 100 {
		limit = 20
	}
	rows, err := store.db.Query(ctx, `
SELECT share.id, share.owner_display_name, share.question,
       share.created_at, share.updated_at, recipient.last_opened_at
FROM knowledge_qa_shares AS share
JOIN knowledge_qa_share_recipients AS recipient ON recipient.share_id=share.id
WHERE recipient.recipient_user_id=$1
ORDER BY recipient.last_opened_at DESC, share.id DESC
LIMIT $2
`, recipientUserID, limit)
	if err != nil {
		return nil, fmt.Errorf("list received Knowledge Q&A shares: %w", err)
	}
	defer rows.Close()
	result := make([]Share, 0)
	for rows.Next() {
		var share Share
		if err := rows.Scan(
			&share.ID,
			&share.OwnerDisplayName,
			&share.Question,
			&share.CreatedAt,
			&share.UpdatedAt,
			&share.LastOpenedAt,
		); err != nil {
			return nil, fmt.Errorf("scan received Knowledge Q&A share summary: %w", err)
		}
		result = append(result, share)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate received Knowledge Q&A shares: %w", err)
	}
	return result, nil
}

func validKnowledgeQAShareToken(token string) bool {
	if !strings.HasPrefix(token, knowledgeQAShareTokenPrefix) {
		return false
	}
	raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(token, knowledgeQAShareTokenPrefix))
	return err == nil && len(raw) == knowledgeQAShareTokenBytes
}

func shareSelectSQL(lastOpenedExpression string) string {
	return `
SELECT share.id, share.source_workspace_id, share.source_organization_id,
       share.source_session_id, share.source_turn_id, share.owner_user_id,
       share.owner_display_name, share.question, share.answer,
       share.created_at, share.updated_at, ` + lastOpenedExpression + `
FROM knowledge_qa_shares AS share
`
}

type knowledgeQAShareScanner interface {
	Scan(dest ...any) error
}

func scanShare(scanner knowledgeQAShareScanner) (Share, error) {
	var share Share
	if err := scanner.Scan(
		&share.ID,
		&share.SourceWorkspaceID,
		&share.SourceOrganizationID,
		&share.SourceSessionID,
		&share.SourceTurnID,
		&share.OwnerUserID,
		&share.OwnerDisplayName,
		&share.Question,
		&share.Answer,
		&share.CreatedAt,
		&share.UpdatedAt,
		&share.LastOpenedAt,
	); err != nil {
		if coredb.IsNotFound(err) {
			return Share{}, ErrNotFound
		}
		return Share{}, fmt.Errorf("scan Knowledge Q&A share: %w", err)
	}
	return share, nil
}
