package tender

import (
	"archive/zip"
	"context"
	"errors"
	"fmt"
	"io"
	"mime"
	"path"
	"path/filepath"
	"strings"
	"time"
	"tm/internal/company"
	"tm/internal/customer"
	"tm/pkg/ai_summarizer"
	"tm/pkg/logger"
	orm "tm/pkg/mongo"
	"tm/pkg/response"
)

// AISummarizerClient defines the interface for on-demand AI operations.
type AISummarizerClient interface {
	FetchSummaryOnDemand(ctx context.Context, reqBody ai_summarizer.SummarizeRequest) (*ai_summarizer.SummarizeResponse, error)
	FetchTranslationOnDemand(ctx context.Context, reqBody ai_summarizer.TranslateRequest) (*ai_summarizer.TranslateResponse, error)
	FetchAnalyzeOnDemand(ctx context.Context, reqBody ai_summarizer.AnalyzeRequest) (*ai_summarizer.AnalyzeResponse, error)
	TriggerTranslateBatch(ctx context.Context, tenders []ai_summarizer.TenderRef, languages []string) (*ai_summarizer.BatchAcceptedResponse, error)
	TriggerSummarizeBatch(ctx context.Context, tenders []ai_summarizer.TenderRef) (*ai_summarizer.BatchAcceptedResponse, error)
}

// AISummarizerStorage defines the interface for fetching summaries from MinIO storage.
// Paths follow the AI team's procedure-scoped layout: artefacts for a notice live under
// PROC_<contractFolderID>/<noticePublicationID>/ in the shared bucket.
type AISummarizerStorage interface {
	GetSummaryFromStorage(ctx context.Context, contractFolderID, noticePublicationID string) (string, error)
	GetDocumentSummariesFromStorage(ctx context.Context, contractFolderID, noticePublicationID string) ([]ai_summarizer.DocumentSummary, error)
	GetTranslationFromStorage(ctx context.Context, contractFolderID, noticePublicationID, language string) (ai_summarizer.StoredTranslation, error)
	ListDocuments(ctx context.Context, contractFolderID, noticePublicationID string) ([]ai_summarizer.StoredDocument, error)
	DownloadDocument(ctx context.Context, objectName string) (io.ReadCloser, error)
}

// TenderDocumentDownload contains an authorized document stream.
type TenderDocumentDownload struct {
	Reader      io.ReadCloser
	Filename    string
	ContentType string
	Size        int64
}

// TenderService interface defines tender business logic operations
type Service interface {
	// Tender operations
	GetByID(ctx context.Context, id string, language string) (*TenderResponse, error)
	GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error)
	Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
	Delete(ctx context.Context, id string) error
	Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
	Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
	GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error)
	GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error)
	DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error)
	DownloadDocument(ctx context.Context, id, filename string) (*TenderDocumentDownload, error)

	// AI summarization operations
	GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
	TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error)
	TriggerAIAnalyze(ctx context.Context, id string) (*AIAnalyzeResponse, error)
	TriggerAITranslate(ctx context.Context, id, language string) (*AITranslateResponse, error)
	GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error)
	// ListTendersWithScrapedDocuments lists tenders that have documents in MinIO. When syncMongoMetadata is true,
	// each tender is re-listed from MinIO and scraped document metadata is persisted to Mongo (slow).
	ListTendersWithScrapedDocuments(ctx context.Context, pagination *response.Pagination, syncMongoMetadata bool) (*TendersWithScrapedDocumentsResponse, error)
	// SyncScrapedDocumentsFromStorage lists documents in MinIO for a tender and persists scraped metadata to Mongo.
	SyncScrapedDocumentsFromStorage(ctx context.Context, contractFolderID, noticePublicationID string) error

	// Maintenance operations
	UpdateExpiredTenders(ctx context.Context) error
	CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error
}

// tenderService implements TenderService interface
type tenderService struct {
	repository          TenderRepository
	companyService      company.Service
	customerService     customer.Service
	logger              logger.Logger
	aiSummarizerClient  AISummarizerClient
	aiSummarizerStorage AISummarizerStorage
	defaultLanguage     string
}

const (
	aiSummaryEnrichmentTimeout     = 2 * time.Second
	aiTranslationEnrichmentTimeout = 2 * time.Second
)

var (
	ErrTenderDocumentStorageUnavailable = errors.New("tender document storage is unavailable")
	ErrTenderDocumentNotFound           = errors.New("tender document not found")
)

// NewService creates a new tender service
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger, aiClient AISummarizerClient, aiStorage AISummarizerStorage, defaultLanguage string) Service {
	if strings.TrimSpace(defaultLanguage) == "" {
		defaultLanguage = "en"
	}

	return &tenderService{
		repository:          repository,
		companyService:      companyService,
		customerService:     customerService,
		logger:              logger,
		aiSummarizerClient:  aiClient,
		aiSummarizerStorage: aiStorage,
		defaultLanguage:     strings.ToLower(defaultLanguage),
	}
}

// GetByID retrieves a tender by ID. When language is empty, the service default (usually en)
// is used to overlay title/description from MinIO translations when available.
func (s *tenderService) GetByID(ctx context.Context, id string, language string) (*TenderResponse, error) {
	s.logger.Info("Retrieving tender by ID", map[string]interface{}{
		"tender_id": id,
		"language":  s.resolveDetailLanguage(language),
	})

	tender, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to retrieve tender", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to retrieve tender: %w", err)
	}

	return s.buildDetailResponse(ctx, tender, language), nil
}

// GetByIDs loads multiple tenders for list endpoints without MinIO or AI enrichment.
func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) {
	if len(ids) == 0 {
		return map[string]*TenderResponse{}, nil
	}

	tenders, err := s.repository.GetByIDs(ctx, ids)
	if err != nil {
		s.logger.Error("Failed to retrieve tenders by IDs", map[string]interface{}{
			"count": len(ids),
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to retrieve tenders: %w", err)
	}

	out := make(map[string]*TenderResponse, len(tenders))
	for i := range tenders {
		resp := tenders[i].ToResponseWithLanguage("")
		out[resp.ID] = resp
	}

	return out, nil
}

// buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary.
func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse {
	resp := tender.ToResponseWithLanguage("")
	s.enrichWithTranslation(ctx, tender, resp, language)
	s.enrichWithAISummary(ctx, tender, resp)
	return resp
}

// enrichWithAISummary loads overall_summary from MinIO (PROC_<contractFolderID>/<noticePublicationID>/tender.json)
// and attaches it to the response. The primary notice publication id is tried first, then related ids.
// A previously stored ai_overall_summary on the tender (already mapped in ToResponse) is kept when MinIO
// has no ready summary. On success the tender document is updated so list/search can return it without
// another MinIO round-trip.
func (s *tenderService) enrichWithAISummary(ctx context.Context, t *Tender, resp *TenderResponse) {
	if t == nil || resp == nil {
		return
	}
	if s.aiSummarizerStorage == nil || strings.TrimSpace(t.ContractFolderID) == "" {
		return
	}

	enrichCtx, cancel := context.WithTimeout(ctx, aiSummaryEnrichmentTimeout)
	defer cancel()

	summary, usedNotice, err := s.lookupSummaryForTender(enrichCtx, t)
	if err != nil {
		s.logger.Debug("AI summary not available for tender", map[string]interface{}{
			"tender_id":          resp.ID,
			"contract_folder_id": t.ContractFolderID,
			"notice_id":          t.NoticePublicationID,
			"error":              err.Error(),
		})
		return
	}

	resp.OverallSummary = summary
	if usedNotice != t.NoticePublicationID {
		s.logger.Debug("AI summary resolved from related notice", map[string]interface{}{
			"tender_id":          resp.ID,
			"contract_folder_id": t.ContractFolderID,
			"primary_notice_id":  t.NoticePublicationID,
			"used_notice_id":     usedNotice,
		})
	}

	s.persistAIOverallSummary(enrichCtx, t, summary)
}

// enrichWithTranslation overlays title/description from MinIO when the pipeline has finished
// for the requested language. Does not call the AI translate API on GET.
func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, resp *TenderResponse, language string) {
	if t == nil || resp == nil {
		return
	}
	if s.aiSummarizerStorage == nil || strings.TrimSpace(t.ContractFolderID) == "" {
		return
	}

	targetLanguage := s.resolveDetailLanguage(language)
	enrichCtx, cancel := context.WithTimeout(ctx, aiTranslationEnrichmentTimeout)
	defer cancel()

	stored, usedNotice, err := s.lookupTranslationForTender(enrichCtx, t, targetLanguage)
	if err != nil {
		s.logger.Debug("Translation not available for tender detail", map[string]interface{}{
			"tender_id":          resp.ID,
			"contract_folder_id": t.ContractFolderID,
			"notice_id":          t.NoticePublicationID,
			"language":           targetLanguage,
			"error":              err.Error(),
		})
		return
	}
	if strings.TrimSpace(stored.Title) == "" {
		return
	}

	resp.Title = stored.Title
	resp.Description = stored.Description
	resp.Language = targetLanguage
	if usedNotice != t.NoticePublicationID {
		s.logger.Debug("Translation resolved from related notice", map[string]interface{}{
			"tender_id":          resp.ID,
			"contract_folder_id": t.ContractFolderID,
			"primary_notice_id":  t.NoticePublicationID,
			"used_notice_id":     usedNotice,
			"language":           targetLanguage,
		})
	}
}

// lookupTranslationForTender returns stored translation text, trying primary then related notice ids.
func (s *tenderService) lookupTranslationForTender(ctx context.Context, t *Tender, language string) (ai_summarizer.StoredTranslation, string, error) {
	if s.aiSummarizerStorage == nil {
		return ai_summarizer.StoredTranslation{}, "", fmt.Errorf("AI summarizer storage is not configured")
	}
	candidates := orderedNoticeCandidates(t)
	if len(candidates) == 0 {
		return ai_summarizer.StoredTranslation{}, "", ai_summarizer.ErrObjectNotFound
	}

	var lastErr error
	for _, noticeID := range candidates {
		stored, err := s.aiSummarizerStorage.GetTranslationFromStorage(ctx, t.ContractFolderID, noticeID, language)
		if err == nil {
			return stored, noticeID, nil
		}
		lastErr = err
		if !errors.Is(err, ai_summarizer.ErrTranslationNotReady) &&
			!errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
			!errors.Is(err, ai_summarizer.ErrNoTranslation) {
			if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
				s.logger.Error("MinIO unavailable while fetching translation", map[string]interface{}{
					"tender_id":          t.ID.Hex(),
					"contract_folder_id": t.ContractFolderID,
					"notice_id":          noticeID,
					"language":           language,
					"error":              err.Error(),
				})
			}
			return ai_summarizer.StoredTranslation{}, "", err
		}
	}
	if lastErr == nil {
		lastErr = ai_summarizer.ErrObjectNotFound
	}
	return ai_summarizer.StoredTranslation{}, "", lastErr
}

// resolveDetailLanguage returns the language code used for detail responses.
func (s *tenderService) resolveDetailLanguage(language string) string {
	clean := strings.ToLower(strings.TrimSpace(language))
	if clean == "" {
		return s.defaultLanguage
	}
	return clean
}

// persistAIOverallSummary stores the resolved summary on the tender for list/search responses.
func (s *tenderService) persistAIOverallSummary(ctx context.Context, t *Tender, summary string) {
	summary = strings.TrimSpace(summary)
	if summary == "" || summary == strings.TrimSpace(t.AIOverallSummary) {
		return
	}

	t.AIOverallSummary = summary
	t.UpdatedAt = time.Now().Unix()
	if err := s.repository.Update(ctx, t); err != nil {
		s.logger.Warn("Failed to persist AI overall summary on tender", map[string]interface{}{
			"tender_id": t.ID.Hex(),
			"error":     err.Error(),
		})
	}
}

// lookupSummaryForTender returns the overall_summary for a tender by trying its
// primary notice publication id first, then each related publication id in
// reverse (newest-first) order. The notice id that actually served the summary
// is returned alongside.
func (s *tenderService) lookupSummaryForTender(ctx context.Context, t *Tender) (string, string, error) {
	if s.aiSummarizerStorage == nil {
		return "", "", fmt.Errorf("AI summarizer storage is not configured")
	}
	candidates := orderedNoticeCandidates(t)
	if len(candidates) == 0 {
		return "", "", ai_summarizer.ErrObjectNotFound
	}

	var lastErr error
	for _, noticeID := range candidates {
		summary, err := s.aiSummarizerStorage.GetSummaryFromStorage(ctx, t.ContractFolderID, noticeID)
		if err == nil {
			return summary, noticeID, nil
		}
		lastErr = err
		// Only "not found" / "not ready" justify trying the next candidate.
		// Anything else (network/storage error) is propagated immediately.
		if !errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
			!errors.Is(err, ai_summarizer.ErrSummaryNotReady) &&
			!errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
			if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
				s.logger.Error("MinIO unavailable while fetching AI summary", map[string]interface{}{
					"tender_id":          t.ID,
					"contract_folder_id": t.ContractFolderID,
					"notice_id":          noticeID,
					"error":              err.Error(),
				})
			}
			return "", noticeID, err
		}
	}
	if lastErr == nil {
		lastErr = ai_summarizer.ErrObjectNotFound
	}
	return "", "", lastErr
}

// orderedNoticeCandidates returns the publication ids to try when resolving
// AI-side artefacts for a (possibly merged) tender, newest-first and deduped.
// The primary NoticePublicationID is tried first, then any older ids stored in
// RelatedNoticePublicationIDs (which we append oldest-first; reverse so newest
// wins).
func orderedNoticeCandidates(t *Tender) []string {
	if t == nil {
		return nil
	}
	seen := make(map[string]struct{}, len(t.RelatedNoticePublicationIDs)+1)
	out := make([]string, 0, len(t.RelatedNoticePublicationIDs)+1)
	add := func(id string) {
		id = strings.TrimSpace(id)
		if id == "" {
			return
		}
		if _, ok := seen[id]; ok {
			return
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	add(t.NoticePublicationID)
	for i := len(t.RelatedNoticePublicationIDs) - 1; i >= 0; i-- {
		add(t.RelatedNoticePublicationIDs[i])
	}
	return out
}

// UpdateTender updates an existing tender
func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) {
	s.logger.Info("Updating tender", map[string]interface{}{
		"tender_id": req.ID,
	})

	// Get existing tender
	tender, err := s.repository.GetByID(ctx, req.ID)
	if err != nil {
		s.logger.Error("Failed to find tender for update", map[string]interface{}{
			"tender_id": req.ID,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to find tender: %w", err)
	}

	// Update fields if provided
	if req.Title != nil {
		tender.Title = *req.Title
	}
	if req.Description != nil {
		tender.Description = *req.Description
	}
	if req.Status != nil {
		tender.Status = *req.Status
	}
	if req.EstimatedValue != nil {
		tender.EstimatedValue = *req.EstimatedValue
	}
	if req.Currency != nil {
		tender.Currency = *req.Currency
	}
	if req.TenderDeadline != nil {
		tender.TenderDeadline = *req.TenderDeadline
	}

	if err := s.repository.Update(ctx, tender); err != nil {
		s.logger.Error("Failed to update tender", map[string]interface{}{
			"tender_id": req.ID,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to update tender: %w", err)
	}

	s.logger.Info("Tender updated successfully", map[string]interface{}{
		"tender_id": tender.ID,
	})

	return s.buildDetailResponse(ctx, tender, ""), nil
}

// Delete deletes a tender
func (s *tenderService) Delete(ctx context.Context, id string) error {
	s.logger.Info("Deleting tender", map[string]interface{}{
		"tender_id": id,
	})

	if err := s.repository.Delete(ctx, id); err != nil {
		s.logger.Error("Failed to delete tender", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return fmt.Errorf("failed to delete tender: %w", err)
	}

	s.logger.Info("Tender deleted successfully", map[string]interface{}{
		"tender_id": id,
	})

	return nil
}

// GetDocumentSummaries retrieves per-document summaries from the AI service MinIO storage.
func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}

	t, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for document summaries", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if t == nil {
		return nil, fmt.Errorf("tender not found")
	}
	if strings.TrimSpace(t.NoticePublicationID) == "" || strings.TrimSpace(t.ContractFolderID) == "" {
		return []DocumentSummary{}, nil
	}
	if s.aiSummarizerStorage == nil {
		return []DocumentSummary{}, nil
	}

	stored, err := s.aiSummarizerStorage.GetDocumentSummariesFromStorage(ctx, t.ContractFolderID, t.NoticePublicationID)
	if errors.Is(err, ai_summarizer.ErrSummaryNotReady) {
		return []DocumentSummary{}, nil
	}
	if errors.Is(err, ai_summarizer.ErrObjectNotFound) {
		return []DocumentSummary{}, nil
	}
	if err != nil {
		if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
			return nil, fmt.Errorf("failed to get document summaries: MinIO connection unavailable: %w", err)
		}
		s.logger.Error("Failed to get document summaries from storage", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get document summaries: %w", err)
	}

	summaries := mapAIDocumentSummaries(stored)
	s.logger.Info("Retrieved document summaries for tender", map[string]interface{}{
		"tender_id":       id,
		"summaries_count": len(summaries),
		"source":          "storage",
	})

	return summaries, nil
}

func mapAIDocumentSummaries(items []ai_summarizer.DocumentSummary) []DocumentSummary {
	if len(items) == 0 {
		return []DocumentSummary{}
	}
	out := make([]DocumentSummary, 0, len(items))
	for _, item := range items {
		out = append(out, DocumentSummary{
			DocumentName:    item.DocumentName,
			Summary:         item.Summary,
			SummaryLanguage: "en",
			DocumentType:    item.DocumentType,
			SummaryModel:    item.SummaryModel,
			Error:           item.Error,
		})
	}
	return out
}

// GetDocuments retrieves scraped document metadata for a tender.
func (s *tenderService) GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}

	tender, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for documents", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if tender == nil {
		return nil, fmt.Errorf("tender not found")
	}

	empty := func() *TenderDocumentsResponse {
		return &TenderDocumentsResponse{
			TenderID:  id,
			Documents: []TenderDocumentResponse{},
		}
	}

	if s.aiSummarizerStorage == nil {
		s.logger.Debug("AI summarizer storage not configured; returning empty tender documents list", map[string]interface{}{
			"tender_id": id,
		})
		return empty(), nil
	}

	if strings.TrimSpace(tender.ContractFolderID) == "" {
		return empty(), nil
	}

	storedDocuments, err := s.listDocumentsForTender(ctx, tender)
	if err != nil {
		s.logger.Warn("Listing tender documents failed; returning empty list", map[string]interface{}{
			"tender_id":          id,
			"contract_folder_id": tender.ContractFolderID,
			"notice_id":          tender.NoticePublicationID,
			"error":              err.Error(),
		})
		storedDocuments = nil
	}

	documents := make([]TenderDocumentResponse, 0, len(storedDocuments))
	for _, doc := range storedDocuments {
		documents = append(documents, TenderDocumentResponse{
			Filename:     doc.DocumentName,
			Size:         doc.Size,
			LastModified: doc.LastModified,
			DocumentType: doc.DocumentType,
		})
	}

	s.logger.Info("Retrieved scraped documents for tender", map[string]interface{}{
		"tender_id":          id,
		"contract_folder_id": tender.ContractFolderID,
		"notice_id":          tender.NoticePublicationID,
		"documents_count":    len(documents),
	})

	return &TenderDocumentsResponse{
		TenderID:  id,
		Documents: documents,
	}, nil
}

// listDocumentsForTender unions document listings across every notice tied to a
// tender (primary + RelatedNoticePublicationIDs), deduplicating by object name.
// This way, a merged tender (e.g. CN + Result for the same procedure) exposes
// the documents from every notice that contributed to it.
func (s *tenderService) listDocumentsForTender(ctx context.Context, t *Tender) ([]ai_summarizer.StoredDocument, error) {
	if s.aiSummarizerStorage == nil {
		return nil, ErrTenderDocumentStorageUnavailable
	}

	candidates := orderedNoticeCandidates(t)
	if len(candidates) == 0 {
		return nil, nil
	}

	seen := make(map[string]struct{})
	var aggregated []ai_summarizer.StoredDocument
	var firstErr error
	for _, noticeID := range candidates {
		docs, err := s.aiSummarizerStorage.ListDocuments(ctx, t.ContractFolderID, noticeID)
		if err != nil {
			// Remember the first error, but keep trying other notices so a
			// missing folder for an older notice does not hide newer docs.
			if firstErr == nil {
				firstErr = err
			}
			continue
		}
		for _, doc := range docs {
			if _, ok := seen[doc.ObjectName]; ok {
				continue
			}
			seen[doc.ObjectName] = struct{}{}
			aggregated = append(aggregated, doc)
		}
	}

	if len(aggregated) == 0 && firstErr != nil {
		return nil, firstErr
	}
	return aggregated, nil
}

// DownloadDocuments returns a ZIP stream containing all documents for a tender.
func (s *tenderService) DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}
	if s.aiSummarizerStorage == nil {
		return nil, ErrTenderDocumentStorageUnavailable
	}

	tender, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for document download", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if tender == nil {
		return nil, fmt.Errorf("tender not found")
	}
	if strings.TrimSpace(tender.ContractFolderID) == "" {
		return nil, ErrTenderDocumentNotFound
	}
	if strings.TrimSpace(tender.NoticePublicationID) == "" {
		return nil, ErrTenderDocumentNotFound
	}

	documents, err := s.listDocumentsForTender(ctx, tender)
	if err != nil {
		s.logger.Error("Failed to list tender documents for archive", map[string]interface{}{
			"tender_id":          id,
			"contract_folder_id": tender.ContractFolderID,
			"notice_id":          tender.NoticePublicationID,
			"error":              err.Error(),
		})
		return nil, fmt.Errorf("failed to list tender documents: %w", err)
	}
	if len(documents) == 0 {
		return nil, ErrTenderDocumentNotFound
	}

	reader, writer := io.Pipe()
	go s.writeDocumentsArchive(ctx, writer, documents)

	filename := fmt.Sprintf("%s-documents.zip", tender.NoticePublicationID)
	s.logger.Info("Tender documents archive download prepared", map[string]interface{}{
		"tender_id":       id,
		"notice_id":       tender.NoticePublicationID,
		"documents_count": len(documents),
	})

	return &TenderDocumentDownload{
		Reader:      reader,
		Filename:    filename,
		ContentType: "application/zip",
	}, nil
}

// DownloadDocument streams a single scraped document for a tender.
func (s *tenderService) DownloadDocument(ctx context.Context, id, filename string) (*TenderDocumentDownload, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}
	if strings.TrimSpace(filename) == "" {
		return nil, fmt.Errorf("document filename cannot be empty")
	}
	if s.aiSummarizerStorage == nil {
		return nil, ErrTenderDocumentStorageUnavailable
	}

	tender, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for document download", map[string]interface{}{
			"tender_id": id,
			"filename":  filename,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if tender == nil {
		return nil, fmt.Errorf("tender not found")
	}
	if strings.TrimSpace(tender.ContractFolderID) == "" {
		return nil, ErrTenderDocumentNotFound
	}

	documents, err := s.listDocumentsForTender(ctx, tender)
	if err != nil {
		s.logger.Error("Failed to list tender documents for download", map[string]interface{}{
			"tender_id":          id,
			"filename":           filename,
			"contract_folder_id": tender.ContractFolderID,
			"error":              err.Error(),
		})
		return nil, fmt.Errorf("failed to list tender documents: %w", err)
	}

	doc, ok := findStoredDocumentByFilename(documents, filename)
	if !ok {
		return nil, ErrTenderDocumentNotFound
	}

	reader, err := s.aiSummarizerStorage.DownloadDocument(ctx, doc.ObjectName)
	if err != nil {
		s.logger.Error("Failed to open tender document from storage", map[string]interface{}{
			"tender_id":   id,
			"filename":    filename,
			"object_name": doc.ObjectName,
			"error":       err.Error(),
		})
		return nil, fmt.Errorf("failed to download document: %w", err)
	}

	downloadName := path.Base(doc.DocumentName)
	contentType := mime.TypeByExtension(filepath.Ext(downloadName))
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	s.logger.Info("Tender document download prepared", map[string]interface{}{
		"tender_id": id,
		"filename":  downloadName,
		"size":      doc.Size,
	})

	return &TenderDocumentDownload{
		Reader:      reader,
		Filename:    downloadName,
		ContentType: contentType,
		Size:        doc.Size,
	}, nil
}

func findStoredDocumentByFilename(documents []ai_summarizer.StoredDocument, filename string) (ai_summarizer.StoredDocument, bool) {
	filename = strings.TrimSpace(filename)
	if filename == "" {
		return ai_summarizer.StoredDocument{}, false
	}

	wantBase := path.Base(filename)
	for _, doc := range documents {
		if doc.DocumentName == filename || path.Base(doc.DocumentName) == wantBase {
			return doc, true
		}
	}
	return ai_summarizer.StoredDocument{}, false
}

func (s *tenderService) writeDocumentsArchive(ctx context.Context, writer *io.PipeWriter, documents []ai_summarizer.StoredDocument) {
	zipWriter := zip.NewWriter(writer)
	usedNames := make(map[string]int, len(documents))

	for _, doc := range documents {
		select {
		case <-ctx.Done():
			zipWriter.Close()
			writer.CloseWithError(ctx.Err())
			return
		default:
		}

		if err := s.addDocumentToArchive(ctx, zipWriter, usedNames, doc); err != nil {
			zipWriter.Close()
			writer.CloseWithError(err)
			return
		}
	}

	if err := zipWriter.Close(); err != nil {
		writer.CloseWithError(err)
		return
	}
	writer.Close()
}

func (s *tenderService) addDocumentToArchive(ctx context.Context, zipWriter *zip.Writer, usedNames map[string]int, doc ai_summarizer.StoredDocument) error {
	reader, err := s.aiSummarizerStorage.DownloadDocument(ctx, doc.ObjectName)
	if err != nil {
		return fmt.Errorf("failed to open document %s: %w", doc.DocumentName, err)
	}
	defer reader.Close()

	entry, err := zipWriter.Create(uniqueArchiveName(usedNames, doc.DocumentName))
	if err != nil {
		return fmt.Errorf("failed to create archive entry for %s: %w", doc.DocumentName, err)
	}

	if _, err := io.Copy(entry, reader); err != nil {
		return fmt.Errorf("failed to write document %s to archive: %w", doc.DocumentName, err)
	}
	return nil
}

func uniqueArchiveName(usedNames map[string]int, filename string) string {
	cleanName := path.Base(filename)
	if cleanName == "." || cleanName == "/" || cleanName == "" {
		cleanName = "document"
	}

	count := usedNames[cleanName]
	usedNames[cleanName] = count + 1
	if count == 0 {
		return cleanName
	}

	extension := filepath.Ext(cleanName)
	base := strings.TrimSuffix(cleanName, extension)
	return fmt.Sprintf("%s-%d%s", base, count+1, extension)
}

// ListTenders retrieves tenders with pagination and filtering
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
	s.logger.Info("Listing tenders", map[string]interface{}{
		"limit":       pagination.Limit,
		"offset":      pagination.Offset,
		"company_id":  form.CompanyID,
		"customer_id": form.CustomerID,
		"from":        form.DeadlineFrom,
		"to":          form.DeadlineTo,
	})

	// If customer ID is provided, get customer keywords and add to search
	if form.CustomerID != nil && *form.CustomerID != "" {
		customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
		if err == nil && customerResp != nil {
			// Only use keywords if status is ready
			if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
				// Combine customer keywords with existing search query
				keywordsQuery := strings.Join(customerResp.Keywords, " ")
				if form.Search != nil && *form.Search != "" {
					combinedSearch := *form.Search + " " + keywordsQuery
					form.Search = &combinedSearch
				} else {
					form.Search = &keywordsQuery
				}
				s.logger.Info("Using customer keywords for search", map[string]interface{}{
					"customer_id": *form.CustomerID,
					"keywords":    customerResp.Keywords,
				})
			} else {
				s.logger.Info("Customer keywords not ready, skipping keyword-based search", map[string]interface{}{
					"customer_id":     *form.CustomerID,
					"keywords_status": customerResp.KeywordsStatus,
				})
			}
		}
	}

	if form.DocumentsScraped {
		if err := s.enrichDocumentsScrapedFilter(ctx, form); err != nil {
			return nil, err
		}
		if len(form.ContractFolderIDsWithDocuments) == 0 {
			return s.emptySearchResponse(pagination), nil
		}
	}

	page, err := s.repository.Search(ctx, form, pagination)
	if err != nil {
		s.logger.Error("Failed to list tenders", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to list tenders: %w", err)
	}

	// If no company ID provided, return tenders without match percentage
	if form.CompanyID == nil || *form.CompanyID == "" {
		tenderResponses := make([]TenderResponse, 0, len(page.Items))
		for _, tender := range page.Items {
			tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
		}

		return &SearchResponse{
			Tenders:  tenderResponses,
			Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
		}, nil
	}

	// Get company CPV codes for match calculation
	comp, err := s.companyService.GetByID(ctx, *form.CompanyID)
	if err != nil {
		s.logger.Error("Failed to get company for match calculation", map[string]interface{}{
			"company_id": *form.CompanyID,
			"error":      err.Error(),
		})
		// Continue without match percentage if company not found
		tenderResponses := make([]TenderResponse, 0, len(page.Items))
		for _, tender := range page.Items {
			tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
		}

		return &SearchResponse{
				Tenders:  tenderResponses,
				Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
			},
			nil
	}

	var companyCPVCodes []string
	if comp.Tags != nil && comp.Tags.CPVCodes != nil {
		companyCPVCodes = comp.Tags.CPVCodes
	}

	s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
		"company_id":        *form.CompanyID,
		"company_cpv_codes": len(companyCPVCodes),
		"total_tenders":     len(page.Items),
	})

	tenderResponses := make([]TenderResponse, 0, len(page.Items))
	for _, tender := range page.Items {
		tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
	}

	return &SearchResponse{
			Tenders:  tenderResponses,
			Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
		},
		nil
}

// Recommend retrieves tenders with pagination and filtering
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
	s.logger.Info("Recommend tenders", map[string]interface{}{
		"company_id":  form.CompanyID,
		"customer_id": form.CustomerID,
	})

	form.Status = []string{string(TenderStatusActive)}

	// Priority: Use customer keywords if available, otherwise use company CPV codes
	if form.CustomerID != nil && *form.CustomerID != "" {
		customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
		if err == nil && customerResp != nil {
			// Only use keywords if status is ready
			if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
				// Use customer keywords for recommendation
				keywordsQuery := strings.Join(customerResp.Keywords, " ")
				form.Search = &keywordsQuery
				s.logger.Info("Using customer keywords for recommendation", map[string]interface{}{
					"customer_id": *form.CustomerID,
					"keywords":    customerResp.Keywords,
				})
			} else {
				s.logger.Info("Customer keywords not ready, falling back to company CPV codes", map[string]interface{}{
					"customer_id":     *form.CustomerID,
					"keywords_status": customerResp.KeywordsStatus,
				})
			}
		}
	}

	// Fallback to company CPV codes if no customer keywords
	if form.Search == nil || *form.Search == "" {
		if form.CompanyID != nil && *form.CompanyID != "" {
			company, err := s.companyService.GetByID(ctx, *form.CompanyID)
			if err != nil {
				s.logger.Error("Failed to get company for CPV codes, skipping CPV-based filtering", map[string]interface{}{
					"company_id": form.CompanyID,
					"error":      err.Error(),
				})
				// Continue without CPV-based filtering instead of failing
			} else if company.Tags != nil && len(company.Tags.CPVCodes) > 0 {
				form.Classifications = company.Tags.CPVCodes
			}
		}
	}

	if form.DocumentsScraped {
		if err := s.enrichDocumentsScrapedFilter(ctx, form); err != nil {
			return nil, err
		}
		if len(form.ContractFolderIDsWithDocuments) == 0 {
			return s.emptySearchResponse(pagination), nil
		}
	}

	page, err := s.repository.Search(ctx, form, pagination)
	if err != nil {
		s.logger.Error("Failed to list tenders", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to list tenders: %w", err)
	}

	tenderResponses := make([]TenderResponse, 0, len(page.Items))
	for _, tender := range page.Items {
		tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
	}

	return &SearchResponse{
		Tenders:  tenderResponses,
		Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
	}, nil
}

// Delete deletes a tender

// GetTenderStatistics retrieves tender statistics
func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) {
	s.logger.Info("Retrieving tender statistics", map[string]interface{}{})

	stats, err := s.repository.GetStatistics(ctx)
	if err != nil {
		s.logger.Error("Failed to retrieve tender statistics", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to retrieve statistics: %w", err)
	}

	return stats, nil
}

// UpdateExpiredTenders updates status of expired tenders
func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error {
	s.logger.Info("Updating expired tenders", map[string]interface{}{})

	// Get tenders that should be expired
	expiredTenders, err := s.repository.GetExpiredTenders(ctx, time.Now().Unix())
	if err != nil {
		s.logger.Error("Failed to get expired tenders", map[string]interface{}{
			"error": err.Error(),
		})
		return fmt.Errorf("failed to get expired tenders: %w", err)
	}

	updatedCount := 0
	for _, tender := range expiredTenders {
		if tender.Status != TenderStatusExpired {
			tender.UpdateStatus(TenderStatusExpired)
			if err := s.repository.Update(ctx, &tender); err != nil {
				s.logger.Error("Failed to update expired tender", map[string]interface{}{
					"tender_id": tender.ID,
					"error":     err.Error(),
				})
				continue
			}
			updatedCount++
		}
	}

	s.logger.Info("Expired tenders updated", map[string]interface{}{
		"total_expired": len(expiredTenders),
		"updated":       updatedCount,
	})

	return nil
}

// CleanupOldData removes old data based on retention policy
func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error {
	s.logger.Info("Cleaning up old data", map[string]interface{}{
		"older_than": olderThan.String(),
		"limit":      pagination.Limit,
		"offset":     pagination.Offset,
	})

	cutoffTime := time.Now().Add(-olderThan).Unix()

	// For now, we'll just log what would be cleaned up
	// In production, you might want to actually delete old data
	form := &SearchForm{
		Status: []string{string(TenderStatusExpired), string(TenderStatusCancelled), string(TenderStatusAwarded)},
	}

	oldPage, err := s.repository.Search(ctx, form, pagination)
	if err != nil {
		s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{
			"error": err.Error(),
		})
		return fmt.Errorf("failed to find old tenders: %w", err)
	}

	cleanupCount := 0
	for _, tender := range oldPage.Items {
		if tender.CreatedAt < cutoffTime {
			cleanupCount++
			// Here you would delete the tender if implementing actual cleanup
			// s.repository.Delete(ctx, tender.ID)
		}
	}

	s.logger.Info("Old data cleanup completed", map[string]interface{}{
		"found_old_tenders": cleanupCount,
		"cutoff_time":       cutoffTime,
	})

	return nil
}

// GetAISummary retrieves the AI-generated summary for a tender.
// It loads from MinIO storage (async pipeline result). When no summary exists yet,
// the pipeline is still running, or storage is unavailable, it returns 200-compatible
// data with an empty overall_summary and a Source hint instead of an error.
func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}

	t, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for AI summary", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if t == nil {
		return nil, fmt.Errorf("tender not found")
	}

	noticeID := strings.TrimSpace(t.NoticePublicationID)
	emptyResp := func(source string) *AISummaryResponse {
		return &AISummaryResponse{
			NoticeID:       noticeID,
			OverallSummary: "",
			Source:         source,
		}
	}

	if noticeID == "" || strings.TrimSpace(t.ContractFolderID) == "" {
		return emptyResp("unavailable"), nil
	}

	if s.aiSummarizerStorage == nil {
		s.logger.Debug("AI summarizer storage not configured; returning empty AI summary", map[string]interface{}{
			"tender_id": id,
		})
		return emptyResp("unavailable"), nil
	}

	summary, usedNotice, err := s.lookupSummaryForTender(ctx, t)
	if err == nil {
		return &AISummaryResponse{
			NoticeID:       usedNotice,
			OverallSummary: summary,
			Source:         "storage",
		}, nil
	}

	s.logger.Info("AI summary not available from storage", map[string]interface{}{
		"tender_id":          id,
		"contract_folder_id": t.ContractFolderID,
		"notice_id":          noticeID,
		"error":              err.Error(),
	})

	if errors.Is(err, ai_summarizer.ErrSummaryNotReady) {
		return emptyResp("pending"), nil
	}
	if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
		return emptyResp("unavailable"), nil
	}
	if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
		s.logger.Error("Cannot retrieve AI summary: MinIO connection unavailable", map[string]interface{}{
			"tender_id":          id,
			"contract_folder_id": t.ContractFolderID,
			"notice_id":          noticeID,
			"error":              err.Error(),
		})
		return nil, fmt.Errorf("failed to get AI summary: MinIO connection unavailable: %w", err)
	}
	return nil, fmt.Errorf("failed to get AI summary: %w", err)
}

// TriggerAISummarize triggers on-demand AI summarization for a tender.
// It calls the external AI service HTTP API to generate summaries.
func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error) {
	if id == "" {
		return nil, ErrEmptyTenderID
	}

	if s.aiSummarizerClient == nil {
		return nil, ErrAINotConfigured
	}

	// Get tender to build the request
	t, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for on-demand summarization", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		if errors.Is(err, orm.ErrDocumentNotFound) {
			return nil, ErrTenderNotFound
		}
		if strings.Contains(err.Error(), "invalid id format") {
			return nil, ErrInvalidTenderID
		}
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}

	if t == nil {
		return nil, ErrTenderNotFound
	}

	if t.NoticePublicationID == "" {
		return nil, ErrTenderMissingNoticeID
	}
	if strings.TrimSpace(t.ContractFolderID) == "" {
		return nil, ErrTenderMissingContractFolderID
	}

	reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID)

	s.logger.Info("Triggering on-demand AI summarization", map[string]interface{}{
		"tender_id":             id,
		"contract_folder_id":    t.ContractFolderID,
		"notice_publication_id": t.NoticePublicationID,
	})

	// Call the AI service
	resp, err := s.aiSummarizerClient.FetchSummaryOnDemand(ctx, reqBody)
	if err != nil {
		s.logger.Error("On-demand AI summarization failed", map[string]interface{}{
			"tender_id":             id,
			"contract_folder_id":    t.ContractFolderID,
			"notice_publication_id": t.NoticePublicationID,
			"error":                 err.Error(),
		})
		return nil, fmt.Errorf("AI summarization request failed: %w", err)
	}

	// Map the response to our response type
	summaries := make([]AIDocumentSummary, 0, len(resp.Summaries))
	for _, s := range resp.Summaries {
		summaries = append(summaries, AIDocumentSummary{
			DocumentName: s.DocumentName,
			DocumentType: s.DocumentType,
			Summary:      s.Summary,
			SummaryModel: s.SummaryModel,
			Error:        s.Error,
		})
	}

	s.logger.Info("On-demand AI summarization completed", map[string]interface{}{
		"tender_id":             id,
		"contract_folder_id":    t.ContractFolderID,
		"notice_publication_id": t.NoticePublicationID,
		"summaries_count":       len(summaries),
	})

	noticePublicationID := resp.ResolvedNoticePublicationID()
	if noticePublicationID == "" {
		noticePublicationID = t.NoticePublicationID
	}
	contractFolderID := strings.TrimSpace(resp.ContractFolderID)
	if contractFolderID == "" {
		contractFolderID = t.ContractFolderID
	}

	return &AISummarizeResponse{
		ContractFolderID:    contractFolderID,
		NoticePublicationID: noticePublicationID,
		Summaries:           summaries,
		OverallSummary:      resp.OverallSummary,
	}, nil
}

// TriggerAIAnalyze triggers on-demand agentic analysis for a tender.
func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAnalyzeResponse, error) {
	if id == "" {
		return nil, ErrEmptyTenderID
	}
	if s.aiSummarizerClient == nil {
		return nil, ErrAINotConfigured
	}

	t, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for on-demand analysis", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		if errors.Is(err, orm.ErrDocumentNotFound) {
			return nil, ErrTenderNotFound
		}
		if strings.Contains(err.Error(), "invalid id format") {
			return nil, ErrInvalidTenderID
		}
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if t == nil {
		return nil, ErrTenderNotFound
	}
	if t.NoticePublicationID == "" {
		return nil, ErrTenderMissingNoticeID
	}
	if strings.TrimSpace(t.ContractFolderID) == "" {
		return nil, ErrTenderMissingContractFolderID
	}

	reqBody := ai_summarizer.NewAnalyzeRequest(t.ContractFolderID, t.NoticePublicationID)
	s.logger.Info("Triggering on-demand AI analysis", map[string]interface{}{
		"tender_id":             id,
		"contract_folder_id":    t.ContractFolderID,
		"notice_publication_id": t.NoticePublicationID,
	})

	resp, err := s.aiSummarizerClient.FetchAnalyzeOnDemand(ctx, reqBody)
	if err != nil {
		s.logger.Error("On-demand AI analysis failed", map[string]interface{}{
			"tender_id":             id,
			"contract_folder_id":    t.ContractFolderID,
			"notice_publication_id": t.NoticePublicationID,
			"error":                 err.Error(),
		})
		return nil, fmt.Errorf("AI analysis request failed: %w", err)
	}

	documents := make([]AIAnalyzeDocument, 0, len(resp.Documents))
	for _, doc := range resp.Documents {
		documents = append(documents, AIAnalyzeDocument{
			Filename:     doc.Filename,
			SizeBytes:    doc.SizeBytes,
			TimesRead:    doc.TimesRead,
			TimesQueried: doc.TimesQueried,
			Description:  doc.Description,
		})
	}

	noticePublicationID := strings.TrimSpace(resp.NoticePublicationID)
	if noticePublicationID == "" {
		noticePublicationID = t.NoticePublicationID
	}
	contractFolderID := strings.TrimSpace(resp.ContractFolderID)
	if contractFolderID == "" {
		contractFolderID = t.ContractFolderID
	}

	s.logger.Info("On-demand AI analysis completed", map[string]interface{}{
		"tender_id":             id,
		"contract_folder_id":    contractFolderID,
		"notice_publication_id": noticePublicationID,
		"iterations":            resp.Iterations,
	})

	return &AIAnalyzeResponse{
		ContractFolderID:    contractFolderID,
		NoticePublicationID: noticePublicationID,
		Summary:             resp.Summary,
		Documents:           documents,
		ToolCallsLog:        resp.ToolCallsLog,
		Iterations:          resp.Iterations,
		Model:               resp.Model,
	}, nil
}

// GetAllAISummaries retrieves all AI-generated summaries from the MinIO storage.
// It lists all tender.json files in the bucket and returns summaries for tenders
// where the AI pipeline has completed processing.
func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error) {

	s.logger.Info("Retrieving all AI summaries from storage", map[string]interface{}{})

	// We need to access the underlying StorageClient to call GetAllSummaries.
	// Since aiSummarizerStorage is an interface, we type-assert to access GetAllSummaries.
	type allSummaries interface {
		GetAllSummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error)
	}

	storage, ok := s.aiSummarizerStorage.(allSummaries)
	if !ok {
		return nil, fmt.Errorf("AI summarizer storage does not support batch retrieval")
	}

	results, err := storage.GetAllSummaries(ctx)
	if err != nil {
		fields := map[string]interface{}{
			"error": err.Error(),
		}
		if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
			s.logger.Error("Failed to list AI summaries: MinIO connection unavailable", fields)
			return nil, fmt.Errorf("failed to get all AI summaries: MinIO connection unavailable: %w", err)
		}
		s.logger.Error("Failed to get all AI summaries from MinIO", fields)
		return nil, fmt.Errorf("failed to get all AI summaries: %w", err)
	}

	s.logger.Info("Retrieved all AI summaries", map[string]interface{}{
		"total_count": len(results),
	})

	return results, nil
}

// ListTendersWithScrapedDocuments lists database tenders whose contract_folder_id has
// documents in MinIO. By default uses counts from the initial MinIO procedure scan only (fast).
// Set syncMongoMetadata=true to list every notice folder again and persist ScrapedDocuments to Mongo (slow).
func (s *tenderService) ListTendersWithScrapedDocuments(ctx context.Context, pagination *response.Pagination, syncMongoMetadata bool) (*TendersWithScrapedDocumentsResponse, error) {
	s.logger.Info("Listing tenders with scraped documents from MinIO", map[string]interface{}{
		"limit":               pagination.Limit,
		"offset":              pagination.Offset,
		"sync_mongo_metadata": syncMongoMetadata,
	})

	procedures, err := s.listProceduresWithDocuments(ctx)
	if err != nil {
		fields := map[string]interface{}{"error": err.Error()}
		if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
			s.logger.Error("Failed to list scraped documents: MinIO unavailable", fields)
			return nil, fmt.Errorf("failed to list tenders with scraped documents: MinIO connection unavailable: %w", err)
		}
		s.logger.Error("Failed to list procedure documents from MinIO", fields)
		return nil, fmt.Errorf("failed to list tenders with scraped documents: %w", err)
	}

	folderIDs := make([]string, 0, len(procedures))
	for _, proc := range procedures {
		folderIDs = append(folderIDs, proc.ContractFolderID)
	}

	tenderByFolder, err := s.repository.GetLatestByContractFolderIDs(ctx, folderIDs)
	if err != nil {
		return nil, fmt.Errorf("failed to load tenders by contract_folder_id: %w", err)
	}

	matched := make([]TenderScrapedDocumentsItem, 0, len(procedures))
	for _, proc := range procedures {
		if proc.DocumentCount <= 0 {
			continue
		}
		tender, ok := tenderByFolder[strings.TrimSpace(proc.ContractFolderID)]
		if tender == nil || !ok {
			continue
		}

		item := tender.ToTenderScrapedDocumentsItem()
		item.DocumentCount = proc.DocumentCount
		item.DocumentsScrapedAt = proc.LatestModified

		if syncMongoMetadata {
			docs, listErr := s.listDocumentsForTender(ctx, tender)
			if listErr != nil {
				if errors.Is(listErr, ai_summarizer.ErrMinIOUnavailable) {
					return nil, fmt.Errorf("failed to list tenders with scraped documents: MinIO connection unavailable: %w", listErr)
				}
				s.logger.Debug("Skipping tender: could not list documents from MinIO", map[string]interface{}{
					"tender_id":          tender.ID.Hex(),
					"contract_folder_id": proc.ContractFolderID,
					"error":              listErr.Error(),
				})
				continue
			}
			if len(docs) == 0 {
				continue
			}
			s.persistScrapedDocuments(ctx, tender, docs)
			item.DocumentCount = len(docs)
			if tender.ProcessingMetadata.DocumentsScrapedAt != 0 {
				item.DocumentsScrapedAt = tender.ProcessingMetadata.DocumentsScrapedAt
			}
		}

		matched = append(matched, item)
	}

	total := int64(len(matched))
	start := pagination.Offset
	if start > len(matched) {
		start = len(matched)
	}
	end := start + pagination.Limit
	if end > len(matched) {
		end = len(matched)
	}
	page := matched[start:end]
	hasMore := int64(end) < total

	s.logger.Info("Listed tenders with scraped documents", map[string]interface{}{
		"count":       len(page),
		"total_count": total,
	})

	return &TendersWithScrapedDocumentsResponse{
		Tenders:  page,
		Metadata: pagination.ListMeta(total, "", hasMore, pagination.Offset),
	}, nil
}

// persistScrapedDocuments updates the tender when MinIO has scraped documents for its procedure.
func (s *tenderService) persistScrapedDocuments(ctx context.Context, t *Tender, docs []ai_summarizer.StoredDocument) {
	if t == nil || len(docs) == 0 {
		return
	}

	bucket := ""
	if named, ok := s.aiSummarizerStorage.(interface{ BucketName() string }); ok {
		bucket = named.BucketName()
	}

	scraped := make([]ScrapedDocument, len(docs))
	var latestModified int64
	for i, doc := range docs {
		scraped[i] = ScrapedDocument{
			Filename:   doc.DocumentName,
			ObjectName: doc.ObjectName,
			BucketName: bucket,
			Size:       doc.Size,
			ScrapedAt:  doc.LastModified,
		}
		if doc.LastModified > latestModified {
			latestModified = doc.LastModified
		}
	}

	unchanged := t.ProcessingMetadata.DocumentsScraped &&
		len(t.ScrapedDocuments) == len(scraped)
	if unchanged {
		return
	}

	t.ProcessingMetadata.DocumentsScraped = true
	if t.ProcessingMetadata.DocumentsScrapedAt == 0 {
		if latestModified > 0 {
			t.ProcessingMetadata.DocumentsScrapedAt = latestModified
		} else {
			t.ProcessingMetadata.DocumentsScrapedAt = time.Now().Unix()
		}
	}
	t.ScrapedDocuments = scraped
	t.UpdatedAt = time.Now().Unix()

	if err := s.repository.Update(ctx, t); err != nil {
		s.logger.Warn("Failed to persist scraped documents on tender", map[string]interface{}{
			"tender_id":          t.ID.Hex(),
			"contract_folder_id": t.ContractFolderID,
			"error":              err.Error(),
		})
	}
}

type procedureDocumentLister interface {
	ListProceduresWithDocuments(ctx context.Context) ([]ai_summarizer.ProcedureDocumentsSummary, error)
}

func (s *tenderService) procedureDocumentStorage() (procedureDocumentLister, error) {
	if s.aiSummarizerStorage == nil {
		return nil, fmt.Errorf("document storage is not configured")
	}
	storage, ok := s.aiSummarizerStorage.(procedureDocumentLister)
	if !ok {
		return nil, fmt.Errorf("document storage does not support procedure listing")
	}
	return storage, nil
}

func (s *tenderService) listProceduresWithDocuments(ctx context.Context) ([]ai_summarizer.ProcedureDocumentsSummary, error) {
	storage, err := s.procedureDocumentStorage()
	if err != nil {
		return nil, err
	}
	procedures, err := storage.ListProceduresWithDocuments(ctx)
	if err != nil {
		return nil, err
	}
	return procedures, nil
}

func (s *tenderService) enrichDocumentsScrapedFilter(ctx context.Context, form *SearchForm) error {
	procedures, err := s.listProceduresWithDocuments(ctx)
	if err != nil {
		s.logger.Error("Failed to resolve tenders with scraped documents from MinIO", map[string]interface{}{
			"error": err.Error(),
		})
		if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
			return fmt.Errorf("failed to resolve tenders with scraped documents: MinIO connection unavailable: %w", err)
		}
		return fmt.Errorf("failed to resolve tenders with scraped documents: %w", err)
	}

	folderIDs := make([]string, 0, len(procedures))
	for _, proc := range procedures {
		if proc.DocumentCount <= 0 {
			continue
		}
		if id := strings.TrimSpace(proc.ContractFolderID); id != "" {
			folderIDs = append(folderIDs, id)
		}
	}
	form.ContractFolderIDsWithDocuments = folderIDs
	return nil
}

func (s *tenderService) emptySearchResponse(pagination *response.Pagination) *SearchResponse {
	return &SearchResponse{
		Tenders:  []TenderResponse{},
		Metadata: pagination.ListMeta(0, "", false, pagination.Offset),
	}
}

// SyncScrapedDocumentsFromStorage lists documents in MinIO for a tender and persists scraped metadata to Mongo.
func (s *tenderService) SyncScrapedDocumentsFromStorage(ctx context.Context, contractFolderID, noticePublicationID string) error {
	contractFolderID = strings.TrimSpace(contractFolderID)
	noticePublicationID = strings.TrimSpace(noticePublicationID)
	if contractFolderID == "" {
		return fmt.Errorf("contract folder ID is required")
	}

	var t *Tender
	var err error
	if noticePublicationID != "" {
		t, err = s.repository.GetByNoticePublicationID(ctx, noticePublicationID)
	} else {
		byFolder, lookupErr := s.repository.GetLatestByContractFolderIDs(ctx, []string{contractFolderID})
		err = lookupErr
		if lookupErr == nil {
			t = byFolder[contractFolderID]
		}
	}
	if err != nil {
		s.logger.Error("Failed to load tender for scraped document sync", map[string]interface{}{
			"contract_folder_id":    contractFolderID,
			"notice_publication_id": noticePublicationID,
			"error":                 err.Error(),
		})
		return fmt.Errorf("failed to load tender for scraped document sync: %w", err)
	}
	if t == nil {
		s.logger.Warn("Skipping scraped document metadata sync: tender not found", map[string]interface{}{
			"contract_folder_id":    contractFolderID,
			"notice_publication_id": noticePublicationID,
		})
		return nil
	}

	docs, err := s.listDocumentsForTender(ctx, t)
	if err != nil {
		s.logger.Error("Failed to list scraped documents from storage", map[string]interface{}{
			"tender_id":             t.ID.Hex(),
			"contract_folder_id":    contractFolderID,
			"notice_publication_id": noticePublicationID,
			"error":                 err.Error(),
		})
		return fmt.Errorf("failed to list scraped documents from storage: %w", err)
	}
	if len(docs) == 0 {
		return nil
	}

	s.persistScrapedDocuments(ctx, t, docs)
	return nil
}

// TriggerAITranslate triggers on-demand AI translation for a tender. Results are
// read from MinIO when available, otherwise fetched from the AI service (which persists to MinIO).
func (s *tenderService) TriggerAITranslate(ctx context.Context, id, language string) (*AITranslateResponse, error) {
	if id == "" {
		return nil, fmt.Errorf("tender ID cannot be empty")
	}
	if s.aiSummarizerClient == nil {
		return nil, fmt.Errorf("AI summarizer service is not configured")
	}

	targetLanguage := strings.ToLower(strings.TrimSpace(language))
	if targetLanguage == "" {
		targetLanguage = s.defaultLanguage
	}

	t, err := s.repository.GetByID(ctx, id)
	if err != nil {
		s.logger.Error("Failed to get tender for AI translation", map[string]interface{}{
			"tender_id": id,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("failed to get tender: %w", err)
	}
	if t == nil {
		return nil, fmt.Errorf("tender not found")
	}
	if t.NoticePublicationID == "" {
		return nil, fmt.Errorf("tender has no notice publication ID")
	}
	if strings.TrimSpace(t.ContractFolderID) == "" {
		return nil, fmt.Errorf("tender has no contract folder ID")
	}

	title, description, fromAPI, err := s.resolveTranslation(ctx, t, targetLanguage, ai_summarizer.TranslationRequestSourceManualTrigger)
	if err != nil {
		s.logger.Error("AI translation failed", map[string]interface{}{
			"tender_id": id,
			"notice_id": t.NoticePublicationID,
			"language":  targetLanguage,
			"error":     err.Error(),
		})
		return nil, fmt.Errorf("AI translation request failed: %w", err)
	}

	if fromAPI {
		s.logger.Info("Manual AI translation request succeeded", map[string]interface{}{
			"tender_id": id,
			"notice_id": t.NoticePublicationID,
			"language":  targetLanguage,
		})
	}

	return s.buildAITranslateResponse(t, targetLanguage, title, description), nil
}

func (s *tenderService) buildAITranslateResponse(t *Tender, language, title, description string) *AITranslateResponse {
	return &AITranslateResponse{
		ContractFolderID:      t.ContractFolderID,
		NoticePublicationID:   t.NoticePublicationID,
		Language:              language,
		TranslatedTitle:       title,
		TranslatedDescription: description,
	}
}

func (s *tenderService) resolveTranslation(ctx context.Context, t *Tender, language, requestSource string) (string, string, bool, error) {
	if s.aiSummarizerStorage != nil {
		stored, _, err := s.lookupTranslationForTender(ctx, t, language)
		if err == nil && strings.TrimSpace(stored.Title) != "" {
			return stored.Title, stored.Description, false, nil
		}
		if err != nil &&
			!errors.Is(err, ai_summarizer.ErrTranslationNotReady) &&
			!errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
			!errors.Is(err, ai_summarizer.ErrNoTranslation) {
			s.logger.Debug("Translation not available from storage", map[string]interface{}{
				"tender_id": t.ID.Hex(),
				"language":  language,
				"error":     err.Error(),
			})
		}
	}

	req := ai_summarizer.NewTranslateRequest(
		t.ContractFolderID,
		t.NoticePublicationID,
		language,
		t.Title,
		t.Description,
	)
	req.RequestSource = requestSource
	resp, err := s.aiSummarizerClient.FetchTranslationOnDemand(ctx, req)
	if err != nil {
		return "", "", false, err
	}

	title := strings.TrimSpace(resp.TranslatedTitle)
	description := resp.TranslatedDescription
	if s.aiSummarizerStorage != nil {
		if stored, _, storageErr := s.lookupTranslationForTender(ctx, t, language); storageErr == nil && strings.TrimSpace(stored.Title) != "" {
			title = stored.Title
			description = stored.Description
		}
	}
	if title == "" {
		title = strings.TrimSpace(resp.TranslatedTitle)
	}

	return title, description, true, nil
}

func (s *tenderService) pickResponseLanguage(language *string) string {
	if language != nil {
		clean := strings.ToLower(strings.TrimSpace(*language))
		if clean != "" {
			return clean
		}
	}
	return s.defaultLanguage
}
