package pipeline

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"textmachine/backend/internal/config"
	"textmachine/backend/internal/llm"
	"textmachine/backend/internal/store"
)

// resume.go: обслуживание уже РЕШЁННОГО чанк×стадии из chunk_status/чекпоинтов —
// $0, без вызова провайдера; ok-стадия отдаёт авторитетный текст следующей стадии,
// терминальный флаг никогда не переатакуется (анти-ведж №1).

// resumeFromChunkStatus serves a chunk×stage whose disposition is already
// resolved (read before any render — no re-billing). An ok stage returns its
// authoritative checkpoint text to feed the next stage; a flagged stage returns
// the flag WITHOUT re-attacking (a terminal flag never re-enters the paid loop).
func (r *Runner) resumeFromChunkStatus(ctx context.Context, st config.Stage, ch Chunk, cs *store.ChunkStatus) (*StageResult, error) {
	sr := &StageResult{
		Stage: st.Name, Role: st.Role, Model: st.Model, FromResume: true,
		CostUSD: 0, CumCostUSD: cs.CostUSD,
		Disposition: Disposition(cs.Disposition), FlagReason: FlagReason(cs.FlagReason),
		Detail: cs.Detail, Attempts: cs.Attempts,
		// Escalation attribution now travels on chunk_status (D15.3), so it is preserved
		// across resume for BOTH an ok escalation AND a flagged-then-escalated chunk (the
		// status read-model reads it without re-opening the checkpoint).
		Escalated: cs.Escalated, EscalationModel: cs.EscalationModel,
	}
	if cs.Disposition == string(DispOK) {
		cp, err := r.Store.GetCheckpoint(cs.FinalHash)
		if err != nil {
			return nil, fmt.Errorf("pipeline: read final checkpoint %.12s for %s/ch%d/chunk%d/%s: %w",
				cs.FinalHash, r.Book.BookID, ch.Chapter, ch.ChunkIdx, st.Name, err)
		}
		if cp == nil || strings.TrimSpace(cp.ResponseText) == "" {
			// chunk_status ok is only written AFTER its checkpoint is durably
			// settled, so this is a torn/tampered store — fail loud rather than
			// feed the next stage an empty draft.
			return nil, fmt.Errorf("pipeline: chunk_status ok for %s/ch%d/chunk%d/%s references checkpoint %.12s which is missing/empty — inconsistent store",
				r.Book.BookID, ch.Chapter, ch.ChunkIdx, st.Name, cs.FinalHash)
		}
		var usage llm.Usage
		if uerr := json.Unmarshal([]byte(cp.UsageJSON), &usage); uerr != nil {
			r.Log.DebugContext(ctx, "checkpoint usage_json unreadable; tokens report as zero", "hash", cs.FinalHash[:12], "err", uerr)
		}
		sr.Usage = usage
		sr.Model = cp.ModelActual
		sr.FinishReason = cp.FinishReason
		sr.Text = cp.ResponseText
	}
	rl := r.baseRequestLog(st, ch, st.Model, cs.FinalHash)
	rl.ModelActual = sr.Model
	rl.TMHit, rl.OK = true, cs.Disposition == string(DispOK)
	rl.FinishReason, rl.Degraded = sr.FinishReason, nonOKTag(cs.Disposition, cs.FlagReason)
	r.Store.LogRequest(ctx, r.Log, rl)
	r.Log.InfoContext(ctx, "stage resolved from chunk_status", "stage", st.Name,
		"chapter", ch.Chapter, "chunk", ch.ChunkIdx, "disposition", cs.Disposition, "reason", cs.FlagReason)
	return sr, nil
}

// nonOKTag is the request_log `degraded` value for a resumed disposition row.
func nonOKTag(disposition, reason string) string {
	if disposition == string(DispOK) {
		return ""
	}
	return reason
}
