package routes

import (
	"bssapp-backend/models"
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/gorilla/mux"
	"github.com/lib/pq"
)

var translationLangSet = map[string]struct{}{
	"tr": {},
	"en": {},
	"de": {},
	"it": {},
	"es": {},
	"ru": {},
	"ar": {},
}

var translationStatusSet = map[string]struct{}{
	"pending":  {},
	"approved": {},
	"rejected": {},
}

var translationSourceTypeSet = map[string]struct{}{
	"dummy":   {},
	"postgre": {},
	"mssql":   {},
}

var (
	reHasLetter       = regexp.MustCompile(`[A-Za-zÇĞİÖŞÜçğıöşü]`)
	reBadText         = regexp.MustCompile(`^(GET|POST|PUT|DELETE|OPTIONS|true|false|null|undefined)$`)
	reKeyUnsafe       = regexp.MustCompile(`[^a-z0-9_]+`)
	reVueTemplate     = regexp.MustCompile(`(?is)<template[^>]*>(.*?)</template>`)
	reVueScript       = regexp.MustCompile(`(?is)<script[^>]*>(.*?)</script>`)
	reTemplateAttr    = regexp.MustCompile(`\b(?:label|title|placeholder|aria-label|hint)\s*=\s*['"]([^'"]{2,180})['"]`)
	reTemplateText    = regexp.MustCompile(`>([^<]{3,180})<`)
	reScriptLabelProp = regexp.MustCompile(`\blabel\s*:\s*['"]([^'"]{2,180})['"]`)
	reScriptUIProp    = regexp.MustCompile(`\b(?:label|message|title|placeholder|hint)\s*:\s*['"]([^'"]{2,180})['"]`)
	reTemplateDynamic = regexp.MustCompile(`[{][{]|[}][}]`)
	reCodeLikeText    = regexp.MustCompile(`(?i)(\bconst\b|\blet\b|\bvar\b|\breturn\b|\bfunction\b|=>|\|\||&&|\?\?|//|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()`)
)

var translationNoiseTokens = map[string]struct{}{
	"flat":        {},
	"dense":       {},
	"filled":      {},
	"outlined":    {},
	"borderless":  {},
	"clearable":   {},
	"loading":     {},
	"disable":     {},
	"readonly":    {},
	"hide-bottom": {},
	"stack-label": {},
	"emit-value":  {},
	"map-options": {},
	"use-input":   {},
	"multiple":    {},
	"options":     {},
	"rows":        {},
	"cols":        {},
	"class":       {},
	"style":       {},
}

var translationDummyAllowedVueDirs = []string{
	"pages/",
	"components/",
	"layouts/",
}

var translationDummyAllowedStoreDirs = []string{
	"stores/",
}

type TranslationUpdatePayload struct {
	SourceTextTR   *string `json:"source_text_tr"`
	TranslatedText *string `json:"translated_text"`
	SourceType     *string `json:"source_type"`
	IsManual       *bool   `json:"is_manual"`
	Status         *string `json:"status"`
}

type UpsertMissingPayload struct {
	Items     []UpsertMissingItem `json:"items"`
	Languages []string            `json:"languages"`
}

type UpsertMissingItem struct {
	TKey         string `json:"t_key"`
	SourceTextTR string `json:"source_text_tr"`
}

type SyncSourcesPayload struct {
	AutoTranslate bool     `json:"auto_translate"`
	Languages     []string `json:"languages"`
	Limit         int      `json:"limit"`
	OnlyNew       *bool    `json:"only_new"`
}

type BulkApprovePayload struct {
	IDs []int64 `json:"ids"`
}

type BulkUpdatePayload struct {
	Items []BulkUpdateItem `json:"items"`
}

type TranslateSelectedPayload struct {
	TKeys     []string `json:"t_keys"`
	Languages []string `json:"languages"`
	Limit     int      `json:"limit"`
}

type RuntimeTranslationsResponse struct {
	Lang   string            `json:"lang"`
	ByKey  map[string]string `json:"by_key"`
	ByText map[string]string `json:"by_text"`
}

type BulkUpdateItem struct {
	ID             int64   `json:"id"`
	SourceTextTR   *string `json:"source_text_tr"`
	TranslatedText *string `json:"translated_text"`
	SourceType     *string `json:"source_type"`
	IsManual       *bool   `json:"is_manual"`
	Status         *string `json:"status"`
}

type TranslationSyncOptions struct {
	AutoTranslate bool
	Languages     []string
	Limit         int
	OnlyNew       bool
	TraceID       string
}

type TranslationSyncResult struct {
	SeedCount      int      `json:"seed_count"`
	AffectedCount  int      `json:"affected_count"`
	AutoTranslated int      `json:"auto_translated"`
	TargetLangs    []string `json:"target_languages"`
	TraceID        string   `json:"trace_id"`
	DurationMS     int64    `json:"duration_ms"`
}

type sourceSeed struct {
	TKey       string
	SourceText string
	SourceType string
}

func GetTranslationRowsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		q := strings.TrimSpace(r.URL.Query().Get("q"))
		lang := normalizeTranslationLang(r.URL.Query().Get("lang"))
		status := normalizeTranslationStatus(r.URL.Query().Get("status"))
		sourceType := normalizeTranslationSourceType(r.URL.Query().Get("source_type"))

		manualFilter := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("manual")))
		missingOnly := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("missing"))) == "true"

		limit := 0
		if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
			if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 50000 {
				limit = parsed
			}
		}
		offset := 0
		if raw := strings.TrimSpace(r.URL.Query().Get("offset")); raw != "" {
			if parsed, err := strconv.Atoi(raw); err == nil && parsed >= 0 && parsed <= 1000000 {
				offset = parsed
			}
		}

		clauses := []string{"1=1"}
		args := make([]any, 0, 8)
		argIndex := 1

		if q != "" {
			clauses = append(clauses, fmt.Sprintf("(source_text_tr ILIKE $%d OR translated_text ILIKE $%d)", argIndex, argIndex))
			args = append(args, "%"+q+"%")
			argIndex++
		}

		if lang != "" {
			clauses = append(clauses, fmt.Sprintf("lang_code = $%d", argIndex))
			args = append(args, lang)
			argIndex++
		}

		if status != "" {
			clauses = append(clauses, fmt.Sprintf("status = $%d", argIndex))
			args = append(args, status)
			argIndex++
		}

		if sourceType != "" {
			clauses = append(clauses, fmt.Sprintf("COALESCE(NULLIF(provider_meta->>'source_type',''),'dummy') = $%d", argIndex))
			args = append(args, sourceType)
			argIndex++
		}

		switch manualFilter {
		case "true":
			clauses = append(clauses, "is_manual = true")
		case "false":
			clauses = append(clauses, "is_manual = false")
		}

		if missingOnly {
			clauses = append(clauses, "(translated_text IS NULL OR btrim(translated_text) = '')")
		}

		query := fmt.Sprintf(`
SELECT
  id,
  t_key,
  lang_code,
  COALESCE(NULLIF(provider_meta->>'source_type',''), 'dummy') AS source_type,
  source_text_tr,
  COALESCE(translated_text, '') AS translated_text,
  is_manual,
  status,
  COALESCE(provider, '') AS provider,
  updated_at
FROM mk_translator
WHERE %s
ORDER BY t_key, lang_code
`, strings.Join(clauses, " AND "))
		if limit > 0 {
			query += fmt.Sprintf("LIMIT $%d", argIndex)
			args = append(args, limit)
			argIndex++
		}
		if offset > 0 {
			query += fmt.Sprintf(" OFFSET $%d", argIndex)
			args = append(args, offset)
		}

		rows, err := db.Query(query, args...)
		if err != nil {
			http.Error(w, "translation query error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		list := make([]models.TranslatorRow, 0, 1024)
		for rows.Next() {
			var row models.TranslatorRow
			if err := rows.Scan(
				&row.ID,
				&row.TKey,
				&row.LangCode,
				&row.SourceType,
				&row.SourceTextTR,
				&row.TranslatedText,
				&row.IsManual,
				&row.Status,
				&row.Provider,
				&row.UpdatedAt,
			); err != nil {
				http.Error(w, "translation scan error", http.StatusInternalServerError)
				return
			}
			list = append(list, row)
		}

		if err := rows.Err(); err != nil {
			http.Error(w, "translation rows error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{
			"rows":  list,
			"count": len(list),
		})
	}
}

func GetRuntimeTranslationsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		lang := normalizeRuntimeTranslationLang(firstNonEmpty(
			r.URL.Query().Get("lang"),
			r.Header.Get("Accept-Language"),
		))
		if lang == "" {
			lang = "tr"
		}

		resp := RuntimeTranslationsResponse{
			Lang:   lang,
			ByKey:  map[string]string{},
			ByText: map[string]string{},
		}

		rows, err := db.Query(`
WITH base AS (
  SELECT DISTINCT ON (t_key)
    t_key,
    COALESCE(NULLIF(source_text_tr, ''), translated_text, '') AS source_text_tr
  FROM mk_translator
  WHERE COALESCE(NULLIF(source_text_tr, ''), translated_text, '') <> ''
    AND status = 'approved'
    AND (
      COALESCE(NULLIF(provider_meta->>'source_type', ''), 'dummy') <> 'dummy'
      OR is_manual = true
    )
    AND char_length(COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) <= 180
    AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '[<>{}\[\]]'
    AND position(chr(96) in COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) = 0
    AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '(=>|\|\||&&|\?\?|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()'
  ORDER BY t_key, CASE WHEN lang_code='tr' THEN 0 ELSE 1 END, updated_at DESC
),
target AS (
  SELECT DISTINCT ON (t_key)
    t_key,
    COALESCE(translated_text, '') AS translated_text
  FROM mk_translator
  WHERE lang_code=$1
    AND (status = 'approved' OR is_manual = true)
  ORDER BY t_key, is_manual DESC, updated_at DESC
)
SELECT
  base.t_key,
  base.source_text_tr,
  CASE
    WHEN $1 = 'tr' THEN base.source_text_tr
    ELSE COALESCE(NULLIF(target.translated_text, ''), base.source_text_tr)
  END AS display_text
FROM base
LEFT JOIN target ON target.t_key = base.t_key
`, lang)
		if err != nil {
			http.Error(w, "runtime translations query error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		for rows.Next() {
			var key, sourceText, displayText string
			if err := rows.Scan(&key, &sourceText, &displayText); err != nil {
				http.Error(w, "runtime translations scan error", http.StatusInternalServerError)
				return
			}
			key = strings.TrimSpace(key)
			sourceText = normalizeRuntimeTranslationText(sourceText)
			displayText = normalizeRuntimeTranslationText(displayText)
			if key != "" && displayText != "" {
				resp.ByKey[key] = displayText
			}
			if sourceText != "" && displayText != "" {
				resp.ByText[sourceText] = displayText
			}
		}
		if err := rows.Err(); err != nil {
			http.Error(w, "runtime translations rows error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(resp)
	}
}

func UpdateTranslationRowHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		id, err := strconv.ParseInt(strings.TrimSpace(mux.Vars(r)["id"]), 10, 64)
		if err != nil || id <= 0 {
			http.Error(w, "invalid row id", http.StatusBadRequest)
			return
		}

		var payload TranslationUpdatePayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}

		if payload.Status != nil {
			normalized := normalizeTranslationStatus(*payload.Status)
			if normalized == "" {
				http.Error(w, "invalid status", http.StatusBadRequest)
				return
			}
			payload.Status = &normalized
		}
		if payload.SourceType != nil {
			normalized := normalizeTranslationSourceType(*payload.SourceType)
			if normalized == "" {
				http.Error(w, "invalid source_type", http.StatusBadRequest)
				return
			}
			payload.SourceType = &normalized
		}

		updateQuery := `
UPDATE mk_translator
SET
  source_text_tr = COALESCE($2, source_text_tr),
  translated_text = COALESCE($3, translated_text),
  is_manual = COALESCE($4, is_manual),
  status = COALESCE($5, status),
  provider_meta = CASE
    WHEN $6::text IS NULL THEN provider_meta
    ELSE jsonb_set(COALESCE(provider_meta, '{}'::jsonb), '{source_type}', to_jsonb($6::text), true)
  END,
  updated_at = NOW()
WHERE id = $1
RETURNING
  id,
  t_key,
  lang_code,
  COALESCE(NULLIF(provider_meta->>'source_type',''), 'dummy') AS source_type,
  source_text_tr,
  COALESCE(translated_text, '') AS translated_text,
  is_manual,
  status,
  COALESCE(provider, '') AS provider,
  updated_at
`

		var row models.TranslatorRow
		err = db.QueryRow(
			updateQuery,
			id,
			nullableString(payload.SourceTextTR),
			nullableString(payload.TranslatedText),
			payload.IsManual,
			payload.Status,
			nullableString(payload.SourceType),
		).Scan(
			&row.ID,
			&row.TKey,
			&row.LangCode,
			&row.SourceType,
			&row.SourceTextTR,
			&row.TranslatedText,
			&row.IsManual,
			&row.Status,
			&row.Provider,
			&row.UpdatedAt,
		)
		if err == sql.ErrNoRows {
			http.Error(w, "translation row not found", http.StatusNotFound)
			return
		}
		if err != nil {
			http.Error(w, "translation update error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(row)
	}
}

func UpsertMissingTranslationsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload UpsertMissingPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}

		items := normalizeMissingItems(payload.Items)
		if len(items) == 0 {
			http.Error(w, "items required", http.StatusBadRequest)
			return
		}

		languages := normalizeTargetLanguages(payload.Languages)
		affected, err := upsertMissingRows(db, items, languages, "dummy")
		if err != nil {
			http.Error(w, "upsert missing error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{
			"success":        true,
			"items":          len(items),
			"target_langs":   languages,
			"affected_count": affected,
		})
	}
}

func SyncTranslationSourcesHandler(pgDB *sql.DB, mssqlDB *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload SyncSourcesPayload
		_ = json.NewDecoder(r.Body).Decode(&payload)
		traceID := requestTraceID(r)
		w.Header().Set("X-Trace-ID", traceID)
		start := time.Now()
		onlyNew := payload.OnlyNew == nil || *payload.OnlyNew
		log.Printf(
			"[TranslationSync] trace=%s stage=request auto_translate=%t only_new=%t limit=%d langs=%v",
			traceID,
			payload.AutoTranslate,
			onlyNew,
			payload.Limit,
			payload.Languages,
		)

		result, err := PerformTranslationSync(pgDB, mssqlDB, TranslationSyncOptions{
			AutoTranslate: payload.AutoTranslate,
			Languages:     payload.Languages,
			Limit:         payload.Limit,
			OnlyNew:       onlyNew,
			TraceID:       traceID,
		})
		if err != nil {
			log.Printf(
				"[TranslationSync] trace=%s stage=error duration_ms=%d err=%v",
				traceID,
				time.Since(start).Milliseconds(),
				err,
			)
			http.Error(w, "translation source sync error", http.StatusInternalServerError)
			return
		}

		log.Printf(
			"[TranslationSync] trace=%s stage=response duration_ms=%d seeds=%d affected=%d auto_translated=%d",
			traceID,
			time.Since(start).Milliseconds(),
			result.SeedCount,
			result.AffectedCount,
			result.AutoTranslated,
		)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"success":          true,
			"trace_id":         traceID,
			"result":           result,
			"seed_count":       result.SeedCount,
			"affected_count":   result.AffectedCount,
			"auto_translated":  result.AutoTranslated,
			"target_languages": result.TargetLangs,
		})
	}
}

func TranslateSelectedTranslationsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload TranslateSelectedPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}

		keys := normalizeStringList(payload.TKeys, 5000)
		if len(keys) == 0 {
			http.Error(w, "t_keys required", http.StatusBadRequest)
			return
		}

		targetLangs := normalizeTargetLanguages(payload.Languages)
		limit := payload.Limit
		if limit <= 0 {
			limit = len(keys) * len(targetLangs)
		}
		if limit <= 0 {
			limit = 1000
		}
		if limit > 50000 {
			limit = 50000
		}

		traceID := requestTraceID(r)
		w.Header().Set("X-Trace-ID", traceID)
		start := time.Now()
		log.Printf(
			"[TranslationSelected] trace=%s stage=request keys=%d limit=%d langs=%v",
			traceID,
			len(keys),
			limit,
			targetLangs,
		)

		translatedCount, err := autoTranslatePendingRowsForKeys(db, targetLangs, limit, keys, traceID)
		if err != nil {
			log.Printf(
				"[TranslationSelected] trace=%s stage=error duration_ms=%d err=%v",
				traceID,
				time.Since(start).Milliseconds(),
				err,
			)
			http.Error(w, "translate selected error", http.StatusInternalServerError)
			return
		}

		log.Printf(
			"[TranslationSelected] trace=%s stage=done duration_ms=%d translated=%d",
			traceID,
			time.Since(start).Milliseconds(),
			translatedCount,
		)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"success":          true,
			"trace_id":         traceID,
			"translated_count": translatedCount,
			"key_count":        len(keys),
			"target_languages": targetLangs,
			"duration_ms":      time.Since(start).Milliseconds(),
		})
	}
}

func BulkApproveTranslationsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload BulkApprovePayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}
		ids := normalizeIDListInt64(payload.IDs)
		if len(ids) == 0 {
			http.Error(w, "ids required", http.StatusBadRequest)
			return
		}

		res, err := db.Exec(`
UPDATE mk_translator
SET
  status = 'approved',
  is_manual = true,
  updated_at = NOW(),
  provider_meta = jsonb_set(COALESCE(provider_meta, '{}'::jsonb), '{is_new}', 'false'::jsonb, true)
WHERE id = ANY($1)
`, pq.Array(ids))
		if err != nil {
			http.Error(w, "bulk approve error", http.StatusInternalServerError)
			return
		}
		affected, _ := res.RowsAffected()

		_ = json.NewEncoder(w).Encode(map[string]any{
			"success":        true,
			"affected_count": affected,
		})
	}
}

func BulkUpdateTranslationsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload BulkUpdatePayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}
		if len(payload.Items) == 0 {
			http.Error(w, "items required", http.StatusBadRequest)
			return
		}

		tx, err := db.Begin()
		if err != nil {
			http.Error(w, "transaction start error", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		affected := 0
		for _, it := range payload.Items {
			if it.ID <= 0 {
				continue
			}
			status := normalizeOptionalStatus(it.Status)
			sourceType := normalizeOptionalSourceType(it.SourceType)
			res, err := tx.Exec(`
UPDATE mk_translator
SET
  source_text_tr = COALESCE($2, source_text_tr),
  translated_text = COALESCE($3, translated_text),
  is_manual = COALESCE($4, is_manual),
  status = COALESCE($5, status),
  provider_meta = CASE
    WHEN $6::text IS NULL THEN provider_meta
    ELSE jsonb_set(COALESCE(provider_meta, '{}'::jsonb), '{source_type}', to_jsonb($6::text), true)
  END,
  updated_at = NOW()
WHERE id = $1
`, it.ID, nullableString(it.SourceTextTR), nullableString(it.TranslatedText), it.IsManual, status, sourceType)
			if err != nil {
				http.Error(w, "bulk update error", http.StatusInternalServerError)
				return
			}
			if n, _ := res.RowsAffected(); n > 0 {
				affected += int(n)
			}
		}

		if err := tx.Commit(); err != nil {
			http.Error(w, "transaction commit error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{
			"success":        true,
			"affected_count": affected,
		})
	}
}

func PerformTranslationSync(pgDB *sql.DB, mssqlDB *sql.DB, options TranslationSyncOptions) (TranslationSyncResult, error) {
	traceID := strings.TrimSpace(options.TraceID)
	if traceID == "" {
		traceID = "trsync-" + strconv.FormatInt(time.Now().UnixNano(), 36)
	}
	start := time.Now()
	limit := options.Limit
	if limit <= 0 || limit > 100000 {
		limit = 20000
	}
	targetLangs := normalizeTargetLanguages(options.Languages)
	log.Printf(
		"[TranslationSync] trace=%s stage=start auto_translate=%t only_new=%t limit=%d langs=%v",
		traceID,
		options.AutoTranslate,
		options.OnlyNew,
		limit,
		targetLangs,
	)

	collectStart := time.Now()
	seeds := collectSourceSeeds(pgDB, mssqlDB, limit)
	seeds, reusedByText := reuseExistingSeedKeys(pgDB, seeds)
	log.Printf(
		"[TranslationSync] trace=%s stage=collect done_ms=%d total=%d reused_by_text=%d sources=%s",
		traceID,
		time.Since(collectStart).Milliseconds(),
		len(seeds),
		reusedByText,
		formatSourceCounts(countSeedsBySource(seeds)),
	)
	if options.OnlyNew {
		before := len(seeds)
		filterStart := time.Now()
		seeds = filterNewSeeds(pgDB, seeds)
		log.Printf(
			"[TranslationSync] trace=%s stage=filter_only_new done_ms=%d before=%d after=%d skipped=%d",
			traceID,
			time.Since(filterStart).Milliseconds(),
			before,
			len(seeds),
			before-len(seeds),
		)
	}
	if len(seeds) == 0 {
		return TranslationSyncResult{
			TargetLangs: targetLangs,
			TraceID:     traceID,
			DurationMS:  time.Since(start).Milliseconds(),
		}, nil
	}

	upsertStart := time.Now()
	affected, err := upsertSourceSeeds(pgDB, seeds, targetLangs)
	if err != nil {
		return TranslationSyncResult{}, err
	}
	log.Printf(
		"[TranslationSync] trace=%s stage=upsert done_ms=%d affected=%d",
		traceID,
		time.Since(upsertStart).Milliseconds(),
		affected,
	)

	autoTranslated := 0
	if options.AutoTranslate {
		autoStart := time.Now()
		var autoErr error
		autoTranslated, autoErr = autoTranslatePendingRowsForKeys(pgDB, targetLangs, limit, uniqueSeedKeys(seeds), traceID)
		if autoErr != nil {
			log.Printf(
				"[TranslationSync] trace=%s stage=auto_translate done_ms=%d translated=%d err=%v",
				traceID,
				time.Since(autoStart).Milliseconds(),
				autoTranslated,
				autoErr,
			)
		} else {
			log.Printf(
				"[TranslationSync] trace=%s stage=auto_translate done_ms=%d translated=%d",
				traceID,
				time.Since(autoStart).Milliseconds(),
				autoTranslated,
			)
		}
	}

	result := TranslationSyncResult{
		SeedCount:      len(seeds),
		AffectedCount:  affected,
		AutoTranslated: autoTranslated,
		TargetLangs:    targetLangs,
		TraceID:        traceID,
		DurationMS:     time.Since(start).Milliseconds(),
	}
	log.Printf(
		"[TranslationSync] trace=%s stage=done duration_ms=%d seeds=%d affected=%d auto_translated=%d",
		traceID,
		result.DurationMS,
		result.SeedCount,
		result.AffectedCount,
		result.AutoTranslated,
	)
	return result, nil
}

func upsertMissingRows(db *sql.DB, items []UpsertMissingItem, languages []string, forcedSourceType string) (int, error) {
	tx, err := db.Begin()
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	affected := 0
	for _, it := range items {
		sourceType := forcedSourceType
		if sourceType == "" {
			sourceType = "dummy"
		}

		res, err := tx.Exec(`
INSERT INTO mk_translator
  (t_key, lang_code, source_text_tr, translated_text, is_manual, status, provider, provider_meta)
VALUES
  ($1, 'tr', $2, $2, false, 'approved', 'seed', jsonb_build_object('source_type', $3::text))
ON CONFLICT (t_key, lang_code) DO UPDATE
SET
  source_text_tr = EXCLUDED.source_text_tr,
  provider_meta = jsonb_set(COALESCE(mk_translator.provider_meta, '{}'::jsonb), '{source_type}', to_jsonb($3::text), true),
  updated_at = NOW()
`, it.TKey, it.SourceTextTR, sourceType)
		if err != nil {
			return 0, err
		}
		if n, _ := res.RowsAffected(); n > 0 {
			affected += int(n)
		}

		for _, lang := range languages {
			res, err := tx.Exec(`
INSERT INTO mk_translator
  (t_key, lang_code, source_text_tr, translated_text, is_manual, status, provider, provider_meta)
VALUES
  ($1, $2, $3, NULL, false, 'pending', NULL, jsonb_build_object('source_type', $4::text))
ON CONFLICT (t_key, lang_code) DO UPDATE
SET
  source_text_tr = EXCLUDED.source_text_tr,
  provider_meta = jsonb_set(COALESCE(mk_translator.provider_meta, '{}'::jsonb), '{source_type}', to_jsonb($4::text), true),
  updated_at = NOW()
`, it.TKey, lang, it.SourceTextTR, sourceType)
			if err != nil {
				return 0, err
			}
			if n, _ := res.RowsAffected(); n > 0 {
				affected += int(n)
			}
		}
	}

	if err := tx.Commit(); err != nil {
		return 0, err
	}
	return affected, nil
}

func upsertSourceSeeds(db *sql.DB, seeds []sourceSeed, languages []string) (int, error) {
	tx, err := db.Begin()
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	affected := 0
	for _, seed := range seeds {
		if seed.TKey == "" || seed.SourceText == "" {
			continue
		}
		sourceType := normalizeTranslationSourceType(seed.SourceType)
		if sourceType == "" {
			sourceType = "dummy"
		}

		res, err := tx.Exec(`
INSERT INTO mk_translator
  (t_key, lang_code, source_text_tr, translated_text, is_manual, status, provider, provider_meta)
VALUES
  ($1, 'tr', $2, $2, false, 'approved', 'seed', jsonb_build_object('source_type', $3::text, 'is_new', false))
ON CONFLICT (t_key, lang_code) DO UPDATE
SET
  source_text_tr = EXCLUDED.source_text_tr,
  provider_meta = jsonb_set(
    COALESCE(mk_translator.provider_meta, '{}'::jsonb),
    '{source_type}',
    to_jsonb(COALESCE(NULLIF(mk_translator.provider_meta->>'source_type', ''), $3::text)),
    true
  ),
  updated_at = NOW()
`, seed.TKey, seed.SourceText, sourceType)
		if err != nil {
			return 0, err
		}
		if n, _ := res.RowsAffected(); n > 0 {
			affected += int(n)
		}

		for _, lang := range languages {
			res, err := tx.Exec(`
INSERT INTO mk_translator
  (t_key, lang_code, source_text_tr, translated_text, is_manual, status, provider, provider_meta)
VALUES
  ($1, $2, $3, NULL, false, 'pending', NULL, jsonb_build_object('source_type', $4::text, 'is_new', true))
ON CONFLICT (t_key, lang_code) DO UPDATE
SET
  source_text_tr = EXCLUDED.source_text_tr,
  provider_meta = jsonb_set(
    COALESCE(mk_translator.provider_meta, '{}'::jsonb),
    '{source_type}',
    to_jsonb(COALESCE(NULLIF(mk_translator.provider_meta->>'source_type', ''), $4::text)),
    true
  ),
  updated_at = NOW()
`, seed.TKey, lang, seed.SourceText, sourceType)
			if err != nil {
				return 0, err
			}
			if n, _ := res.RowsAffected(); n > 0 {
				affected += int(n)
			}
		}
	}

	if err := tx.Commit(); err != nil {
		return 0, err
	}
	return affected, nil
}

func collectSourceSeeds(pgDB *sql.DB, mssqlDB *sql.DB, limit int) []sourceSeed {
	seen := map[string]struct{}{}
	out := make([]sourceSeed, 0, limit)

	appendSeed := func(seed sourceSeed) {
		if seed.TKey == "" || seed.SourceText == "" || seed.SourceType == "" {
			return
		}
		key := normalizeSeedTextKey(seed.SourceText)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		out = append(out, seed)
	}

	for _, row := range collectPostgreSeeds(pgDB, limit) {
		appendSeed(row)
		if len(out) >= limit {
			return out
		}
	}
	for _, row := range collectMSSQLSeeds(mssqlDB, limit-len(out)) {
		appendSeed(row)
		if len(out) >= limit {
			return out
		}
	}
	if strings.EqualFold(strings.TrimSpace(os.Getenv("TRANSLATION_DUMMY_SEEDS_ENABLED")), "true") {
		for _, row := range collectDummySeeds(limit - len(out)) {
			appendSeed(row)
			if len(out) >= limit {
				return out
			}
		}
	}

	return out
}

func collectPostgreSeeds(pgDB *sql.DB, limit int) []sourceSeed {
	if pgDB == nil || limit <= 0 {
		return nil
	}
	rows, err := pgDB.Query(`
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position
LIMIT $1
`, limit)
	if err != nil {
		return nil
	}
	defer rows.Close()

	out := make([]sourceSeed, 0, limit)
	for rows.Next() && len(out) < limit {
		var tableName, columnName string
		if err := rows.Scan(&tableName, &columnName); err != nil {
			continue
		}
		text := normalizeDisplayText(columnName)
		key := makeTextBasedSeedKey(text)
		out = append(out, sourceSeed{
			TKey:       key,
			SourceText: text,
			SourceType: "postgre",
		})
	}
	return out
}

func collectMSSQLSeeds(mssqlDB *sql.DB, limit int) []sourceSeed {
	if mssqlDB == nil || limit <= 0 {
		return nil
	}
	maxPerRun := parsePositiveIntEnv("TRANSLATION_MSSQL_SEED_LIMIT", 2500)
	if limit > maxPerRun {
		limit = maxPerRun
	}
	timeoutSec := parsePositiveIntEnv("TRANSLATION_MSSQL_SCHEMA_TIMEOUT_SEC", 20)
	query := fmt.Sprintf(`
SELECT TOP (%d) TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
ORDER BY TABLE_NAME, ORDINAL_POSITION
`, limit)
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
	defer cancel()
	rows, err := mssqlDB.QueryContext(ctx, query)
	if err != nil {
		log.Printf("[TranslationSync] stage=collect_mssql skipped err=%v", err)
		return nil
	}
	defer rows.Close()

	out := make([]sourceSeed, 0, limit)
	for rows.Next() && len(out) < limit {
		var tableName, columnName string
		if err := rows.Scan(&tableName, &columnName); err != nil {
			continue
		}
		text := normalizeDisplayText(columnName)
		key := makeTextBasedSeedKey(text)
		out = append(out, sourceSeed{
			TKey:       key,
			SourceText: text,
			SourceType: "mssql",
		})
	}
	return out
}

func collectDummySeeds(limit int) []sourceSeed {
	if limit <= 0 {
		return nil
	}

	root := detectProjectRoot()
	if root == "" {
		return nil
	}
	uiRoot := filepath.Join(root, "ui", "src")
	if _, err := os.Stat(uiRoot); err != nil {
		return nil
	}

	out := make([]sourceSeed, 0, limit)
	seen := make(map[string]struct{}, limit)

	_ = filepath.WalkDir(uiRoot, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".vue" && ext != ".js" && ext != ".ts" {
			return nil
		}
		if !shouldCollectDummySeedFile(uiRoot, path, ext) {
			return nil
		}

		b, err := os.ReadFile(path)
		if err != nil {
			return nil
		}
		texts := extractVisibleUIText(string(b), ext)
		for _, text := range texts {
			if !isCandidateText(text) {
				continue
			}
			if _, ok := seen[text]; ok {
				continue
			}
			seen[text] = struct{}{}
			key := makeTextBasedSeedKey(text)
			out = append(out, sourceSeed{
				TKey:       key,
				SourceText: text,
				SourceType: "dummy",
			})
			if len(out) >= limit {
				return errors.New("limit reached")
			}
		}
		return nil
	})

	return out
}

func shouldCollectDummySeedFile(uiRoot, fullPath, ext string) bool {
	rel, err := filepath.Rel(uiRoot, fullPath)
	if err != nil {
		return false
	}
	rel = strings.ToLower(filepath.ToSlash(rel))
	if strings.Contains(rel, "/__tests__/") || strings.Contains(rel, "/tests/") || strings.Contains(rel, "/mock/") || strings.Contains(rel, "/mocks/") {
		return false
	}

	if ext == ".vue" {
		for _, prefix := range translationDummyAllowedVueDirs {
			if strings.HasPrefix(rel, prefix) {
				return true
			}
		}
		return false
	}

	if ext == ".js" || ext == ".ts" {
		for _, prefix := range translationDummyAllowedStoreDirs {
			if strings.HasPrefix(rel, prefix) {
				return true
			}
		}
		return false
	}

	return false
}

func extractVisibleUIText(content string, ext string) []string {
	out := make([]string, 0, 32)
	seen := map[string]struct{}{}
	isLikelyAttrNoise := func(text string) bool {
		tokens := strings.Fields(strings.ToLower(text))
		if len(tokens) < 2 || len(tokens) > 16 {
			return false
		}
		matched := 0
		for _, t := range tokens {
			if _, ok := translationNoiseTokens[t]; ok {
				matched++
				continue
			}
			if strings.HasPrefix(t, ":") || strings.HasPrefix(t, "@") || strings.HasPrefix(t, "v-") || strings.HasPrefix(t, "#") {
				matched++
				continue
			}
		}
		return matched == len(tokens)
	}
	appendText := func(raw string) {
		if strings.ContainsAny(raw, "\r\n\t") {
			return
		}
		text := strings.TrimSpace(strings.Join(strings.Fields(raw), " "))
		if text == "" {
			return
		}
		if strings.ContainsAny(text, "<>{}[]`") {
			return
		}
		if strings.Contains(text, "=") || strings.Contains(text, "#") {
			return
		}
		if reTemplateDynamic.MatchString(text) {
			return
		}
		if isLikelyAttrNoise(text) {
			return
		}
		if _, ok := seen[text]; ok {
			return
		}
		seen[text] = struct{}{}
		out = append(out, text)
	}

	switch ext {
	case ".vue":
		template := content
		if m := reVueTemplate.FindStringSubmatch(content); len(m) > 1 {
			template = m[1]
		}
		for _, m := range reTemplateAttr.FindAllStringSubmatch(template, -1) {
			if len(m) > 1 {
				appendText(m[1])
			}
		}
		for _, m := range reTemplateText.FindAllStringSubmatch(template, -1) {
			if len(m) > 1 {
				appendText(m[1])
			}
		}
		script := content
		if m := reVueScript.FindStringSubmatch(content); len(m) > 1 {
			script = m[1]
		}
		for _, m := range reScriptLabelProp.FindAllStringSubmatch(script, -1) {
			if len(m) > 1 {
				appendText(m[1])
			}
		}
		for _, m := range reScriptUIProp.FindAllStringSubmatch(script, -1) {
			if len(m) > 1 {
				appendText(m[1])
			}
		}
	case ".js", ".ts":
		for _, m := range reScriptUIProp.FindAllStringSubmatch(content, -1) {
			if len(m) > 1 {
				appendText(m[1])
			}
		}
	}

	return out
}

func autoTranslatePendingRows(db *sql.DB, langs []string, limit int) (int, error) {
	return autoTranslatePendingRowsForKeys(db, langs, limit, nil, "")
}

func autoTranslatePendingRowsForKeys(db *sql.DB, langs []string, limit int, keys []string, traceID string) (int, error) {
	traceID = strings.TrimSpace(traceID)
	if traceID == "" {
		traceID = "trauto-" + strconv.FormatInt(time.Now().UnixNano(), 36)
	}

	if len(keys) == 0 {
		log.Printf("[TranslationAuto] trace=%s stage=skip reason=no_keys", traceID)
		return 0, nil
	}

	start := time.Now()
	rows, err := db.Query(`
SELECT id, lang_code, source_text_tr
FROM mk_translator
WHERE lang_code = ANY($1)
  AND t_key = ANY($3)
  AND (translated_text IS NULL OR btrim(translated_text) = '')
  AND is_manual = false
ORDER BY updated_at ASC
LIMIT $2
`, pqArray(langs), limit, pq.Array(keys))
	if err != nil {
		return 0, err
	}
	defer rows.Close()

	type pending struct {
		ID   int64
		Lang string
		Text string
	}
	list := make([]pending, 0, limit)
	pendingByLang := map[string]int{}
	sourceChars := 0
	for rows.Next() {
		var p pending
		if err := rows.Scan(&p.ID, &p.Lang, &p.Text); err != nil {
			continue
		}
		if strings.TrimSpace(p.Text) == "" {
			continue
		}
		p.Lang = normalizeTranslationLang(p.Lang)
		if p.Lang == "" {
			continue
		}
		list = append(list, p)
		pendingByLang[p.Lang]++
		sourceChars += len([]rune(strings.TrimSpace(p.Text)))
	}
	if err := rows.Err(); err != nil {
		return 0, err
	}

	log.Printf(
		"[TranslationAuto] trace=%s stage=prepare candidates=%d limit=%d keys=%d langs=%v source_chars=%d pending_by_lang=%s",
		traceID,
		len(list),
		limit,
		len(keys),
		langs,
		sourceChars,
		formatLangCounts(pendingByLang),
	)
	if len(list) == 0 {
		log.Printf(
			"[TranslationAuto] trace=%s stage=done duration_ms=%d translated=0 failed_translate=0 failed_update=0 rps=0.00",
			traceID,
			time.Since(start).Milliseconds(),
		)
		return 0, nil
	}

	done := 0
	failedTranslate := 0
	failedUpdate := 0
	doneByLang := map[string]int{}
	var processedCount int64
	var translatedCount int64
	var failedTranslateCount int64
	var failedUpdateCount int64
	progressEvery := parsePositiveIntEnv("TRANSLATION_AUTO_PROGRESS_EVERY", 100)
	if progressEvery <= 0 {
		progressEvery = 100
	}
	progressSec := parsePositiveIntEnv("TRANSLATION_AUTO_PROGRESS_SEC", 15)
	if progressSec <= 0 {
		progressSec = 15
	}
	progressTicker := time.Duration(progressSec) * time.Second
	lastProgress := time.Now()
	heartbeatDone := make(chan struct{})

	go func() {
		ticker := time.NewTicker(progressTicker)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				processed := int(atomic.LoadInt64(&processedCount))
				translated := int(atomic.LoadInt64(&translatedCount))
				failedTr := int(atomic.LoadInt64(&failedTranslateCount))
				failedUpd := int(atomic.LoadInt64(&failedUpdateCount))
				elapsed := time.Since(start)
				rps := float64(translated)
				if elapsed > 0 {
					rps = float64(translated) / elapsed.Seconds()
				}
				log.Printf(
					"[TranslationAuto] trace=%s stage=heartbeat processed=%d/%d translated=%d failed_translate=%d failed_update=%d elapsed_ms=%d rps=%.2f",
					traceID,
					processed,
					len(list),
					translated,
					failedTr,
					failedUpd,
					elapsed.Milliseconds(),
					rps,
				)
			case <-heartbeatDone:
				return
			}
		}
	}()
	defer close(heartbeatDone)

	for i, p := range list {
		tr, err := callAzureTranslate(p.Text, p.Lang)
		if err != nil || strings.TrimSpace(tr) == "" {
			failedTranslate++
			atomic.StoreInt64(&failedTranslateCount, int64(failedTranslate))
			atomic.StoreInt64(&processedCount, int64(i+1))
			continue
		}
		_, err = db.Exec(`
UPDATE mk_translator
SET translated_text = $2,
    status = 'pending',
    is_manual = false,
    provider = 'azure_translator',
    updated_at = NOW()
WHERE id = $1
`, p.ID, strings.TrimSpace(tr))
		if err != nil {
			failedUpdate++
			atomic.StoreInt64(&failedUpdateCount, int64(failedUpdate))
			atomic.StoreInt64(&processedCount, int64(i+1))
			continue
		}
		done++
		atomic.StoreInt64(&translatedCount, int64(done))
		atomic.StoreInt64(&processedCount, int64(i+1))
		doneByLang[p.Lang]++

		processed := i + 1
		shouldLogProgress := processed%progressEvery == 0 || time.Since(lastProgress) >= progressTicker || processed == len(list)
		if shouldLogProgress {
			elapsed := time.Since(start)
			rps := float64(done)
			if elapsed > 0 {
				rps = float64(done) / elapsed.Seconds()
			}
			log.Printf(
				"[TranslationAuto] trace=%s stage=progress processed=%d/%d translated=%d failed_translate=%d failed_update=%d elapsed_ms=%d rps=%.2f done_by_lang=%s",
				traceID,
				processed,
				len(list),
				done,
				failedTranslate,
				failedUpdate,
				elapsed.Milliseconds(),
				rps,
				formatLangCounts(doneByLang),
			)
			lastProgress = time.Now()
		}
	}

	elapsed := time.Since(start)
	rps := float64(done)
	if elapsed > 0 {
		rps = float64(done) / elapsed.Seconds()
	}
	log.Printf(
		"[TranslationAuto] trace=%s stage=done duration_ms=%d candidates=%d translated=%d failed_translate=%d failed_update=%d rps=%.2f done_by_lang=%s",
		traceID,
		elapsed.Milliseconds(),
		len(list),
		done,
		failedTranslate,
		failedUpdate,
		rps,
		formatLangCounts(doneByLang),
	)
	return done, nil
}

func formatLangCounts(counts map[string]int) string {
	if len(counts) == 0 {
		return "-"
	}
	keys := make([]string, 0, len(counts))
	for k := range counts {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, fmt.Sprintf("%s=%d", k, counts[k]))
	}
	return strings.Join(parts, ",")
}

func filterNewSeeds(pgDB *sql.DB, seeds []sourceSeed) []sourceSeed {
	if pgDB == nil || len(seeds) == 0 {
		return seeds
	}

	keys := uniqueSeedKeys(seeds)
	if len(keys) == 0 {
		return nil
	}
	textKeys := uniqueSeedTextKeys(seeds)

	rows, err := pgDB.Query(`
SELECT DISTINCT t_key, lower(btrim(source_text_tr)) AS text_key
FROM mk_translator
WHERE t_key = ANY($1)
   OR lower(btrim(source_text_tr)) = ANY($2)
`, pq.Array(keys), pq.Array(textKeys))
	if err != nil {
		return seeds
	}
	defer rows.Close()

	existing := make(map[string]struct{}, len(keys))
	existingText := make(map[string]struct{}, len(textKeys))
	for rows.Next() {
		var key string
		var textKey sql.NullString
		if err := rows.Scan(&key, &textKey); err == nil {
			if strings.TrimSpace(key) != "" {
				existing[key] = struct{}{}
			}
			if textKey.Valid {
				t := strings.TrimSpace(textKey.String)
				if t != "" {
					existingText[t] = struct{}{}
				}
			}
		}
	}

	out := make([]sourceSeed, 0, len(seeds))
	for _, seed := range seeds {
		if _, ok := existing[seed.TKey]; ok {
			continue
		}
		if _, ok := existingText[normalizeSeedTextKey(seed.SourceText)]; ok {
			continue
		}
		out = append(out, seed)
	}
	return out
}

func uniqueSeedKeys(seeds []sourceSeed) []string {
	seen := make(map[string]struct{}, len(seeds))
	out := make([]string, 0, len(seeds))
	for _, seed := range seeds {
		if seed.TKey == "" {
			continue
		}
		if _, ok := seen[seed.TKey]; ok {
			continue
		}
		seen[seed.TKey] = struct{}{}
		out = append(out, seed.TKey)
	}
	return out
}

func uniqueSeedTextKeys(seeds []sourceSeed) []string {
	seen := make(map[string]struct{}, len(seeds))
	out := make([]string, 0, len(seeds))
	for _, seed := range seeds {
		k := normalizeSeedTextKey(seed.SourceText)
		if k == "" {
			continue
		}
		if _, ok := seen[k]; ok {
			continue
		}
		seen[k] = struct{}{}
		out = append(out, k)
	}
	return out
}

func reuseExistingSeedKeys(pgDB *sql.DB, seeds []sourceSeed) ([]sourceSeed, int) {
	if pgDB == nil || len(seeds) == 0 {
		return seeds, 0
	}

	textKeys := uniqueSeedTextKeys(seeds)
	if len(textKeys) == 0 {
		return seeds, 0
	}

	rows, err := pgDB.Query(`
SELECT x.text_key, x.t_key
FROM (
  SELECT
    lower(btrim(source_text_tr)) AS text_key,
    t_key,
    ROW_NUMBER() OVER (
      PARTITION BY lower(btrim(source_text_tr))
      ORDER BY id ASC
    ) AS rn
  FROM mk_translator
  WHERE lower(btrim(source_text_tr)) = ANY($1)
) x
WHERE x.rn = 1
`, pq.Array(textKeys))
	if err != nil {
		return seeds, 0
	}
	defer rows.Close()

	existingByText := make(map[string]string, len(textKeys))
	for rows.Next() {
		var textKey, tKey string
		if err := rows.Scan(&textKey, &tKey); err != nil {
			continue
		}
		textKey = strings.TrimSpace(strings.ToLower(textKey))
		tKey = strings.TrimSpace(tKey)
		if textKey == "" || tKey == "" {
			continue
		}
		existingByText[textKey] = tKey
	}

	reused := 0
	for i := range seeds {
		textKey := normalizeSeedTextKey(seeds[i].SourceText)
		if textKey == "" {
			continue
		}
		if existingKey, ok := existingByText[textKey]; ok && existingKey != "" && seeds[i].TKey != existingKey {
			seeds[i].TKey = existingKey
			reused++
		}
	}

	return seeds, reused
}

func countSeedsBySource(seeds []sourceSeed) map[string]int {
	out := map[string]int{
		"dummy":   0,
		"postgre": 0,
		"mssql":   0,
	}
	for _, s := range seeds {
		key := normalizeTranslationSourceType(s.SourceType)
		if key == "" {
			key = "dummy"
		}
		out[key]++
	}
	return out
}

func formatSourceCounts(counts map[string]int) string {
	return fmt.Sprintf("dummy=%d postgre=%d mssql=%d", counts["dummy"], counts["postgre"], counts["mssql"])
}

func requestTraceID(r *http.Request) string {
	if r == nil {
		return "trsync-" + strconv.FormatInt(time.Now().UnixNano(), 36)
	}
	id := strings.TrimSpace(r.Header.Get("X-Request-ID"))
	if id == "" {
		id = strings.TrimSpace(r.Header.Get("X-Correlation-ID"))
	}
	if id == "" {
		id = "trsync-" + strconv.FormatInt(time.Now().UnixNano(), 36)
	}
	return id
}

func callAzureTranslate(sourceText, targetLang string) (string, error) {
	key := strings.TrimSpace(os.Getenv("AZURE_TRANSLATOR_KEY"))
	endpoint := strings.TrimSpace(os.Getenv("AZURE_TRANSLATOR_ENDPOINT"))
	region := strings.TrimSpace(os.Getenv("AZURE_TRANSLATOR_REGION"))
	if key == "" {
		return "", errors.New("AZURE_TRANSLATOR_KEY not set")
	}
	if endpoint == "" {
		return "", errors.New("AZURE_TRANSLATOR_ENDPOINT not set")
	}
	if region == "" {
		return "", errors.New("AZURE_TRANSLATOR_REGION not set")
	}

	sourceLang := strings.TrimSpace(strings.ToLower(os.Getenv("TRANSLATION_SOURCE_LANG")))
	if sourceLang == "" {
		sourceLang = "tr"
	}
	targetLang = normalizeTranslationLang(targetLang)
	if targetLang == "" || targetLang == "tr" {
		return "", fmt.Errorf("invalid target language: %q", targetLang)
	}

	endpoint = strings.TrimRight(endpoint, "/")
	normalizedEndpoint := strings.ToLower(endpoint)
	translatePath := "/translate"
	// Azure custom endpoint requires the translator path with version in URL.
	if strings.Contains(normalizedEndpoint, ".cognitiveservices.azure.com") {
		translatePath = "/translator/text/v3.0/translate"
	}
	baseURL, err := url.Parse(endpoint + translatePath)
	if err != nil {
		return "", fmt.Errorf("invalid AZURE_TRANSLATOR_ENDPOINT: %w", err)
	}
	q := baseURL.Query()
	if translatePath == "/translate" {
		q.Set("api-version", "3.0")
	}
	q.Set("from", sourceLang)
	q.Set("to", targetLang)
	baseURL.RawQuery = q.Encode()

	payload := []map[string]string{
		{"text": sourceText},
	}
	body, _ := json.Marshal(payload)
	req, err := http.NewRequest(http.MethodPost, baseURL.String(), bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Ocp-Apim-Subscription-Key", key)
	req.Header.Set("Ocp-Apim-Subscription-Region", region)
	req.Header.Set("Content-Type", "application/json; charset=UTF-8")

	timeoutSec := parsePositiveIntEnv("TRANSLATION_HTTP_TIMEOUT_SEC", 60)
	client := &http.Client{Timeout: time.Duration(timeoutSec) * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return "", fmt.Errorf("azure translator status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(raw)))
	}

	var result []struct {
		Translations []struct {
			Text string `json:"text"`
			To   string `json:"to"`
		} `json:"translations"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}
	if len(result) == 0 || len(result[0].Translations) == 0 {
		return "", errors.New("azure translator empty response")
	}
	return strings.TrimSpace(result[0].Translations[0].Text), nil
}

func nullableString(v *string) any {
	if v == nil {
		return nil
	}
	s := strings.TrimSpace(*v)
	return s
}

func normalizeTranslationLang(v string) string {
	lang := strings.ToLower(strings.TrimSpace(v))
	if _, ok := translationLangSet[lang]; ok {
		return lang
	}
	return ""
}

func normalizeRuntimeTranslationLang(v string) string {
	raw := strings.ToLower(strings.TrimSpace(v))
	if raw == "" {
		return ""
	}
	if strings.Contains(raw, ",") {
		raw = strings.TrimSpace(strings.Split(raw, ",")[0])
	}
	if strings.Contains(raw, ";") {
		raw = strings.TrimSpace(strings.Split(raw, ";")[0])
	}
	if strings.Contains(raw, "-") {
		raw = strings.TrimSpace(strings.Split(raw, "-")[0])
	}
	if strings.Contains(raw, "_") {
		raw = strings.TrimSpace(strings.Split(raw, "_")[0])
	}
	return normalizeTranslationLang(raw)
}

func normalizeRuntimeTranslationText(v string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(v)), " ")
}

func normalizeTranslationStatus(v string) string {
	status := strings.ToLower(strings.TrimSpace(v))
	if _, ok := translationStatusSet[status]; ok {
		return status
	}
	return ""
}

func normalizeTranslationSourceType(v string) string {
	sourceType := strings.ToLower(strings.TrimSpace(v))
	if _, ok := translationSourceTypeSet[sourceType]; ok {
		return sourceType
	}
	return ""
}

func normalizeTargetLanguages(list []string) []string {
	if len(list) == 0 {
		return []string{"en", "de", "it", "es", "ru", "ar"}
	}

	seen := make(map[string]struct{}, len(list))
	out := make([]string, 0, len(list))
	for _, v := range list {
		lang := normalizeTranslationLang(v)
		if lang == "" || lang == "tr" {
			continue
		}
		if _, ok := seen[lang]; ok {
			continue
		}
		seen[lang] = struct{}{}
		out = append(out, lang)
	}
	if len(out) == 0 {
		return []string{"en", "de", "it", "es", "ru", "ar"}
	}
	return out
}

func normalizeOptionalStatus(v *string) any {
	if v == nil {
		return nil
	}
	s := normalizeTranslationStatus(*v)
	if s == "" {
		return nil
	}
	return s
}

func normalizeOptionalSourceType(v *string) any {
	if v == nil {
		return nil
	}
	s := normalizeTranslationSourceType(*v)
	if s == "" {
		return nil
	}
	return s
}

func normalizeMissingItems(items []UpsertMissingItem) []UpsertMissingItem {
	seen := make(map[string]struct{}, len(items))
	out := make([]UpsertMissingItem, 0, len(items))

	for _, it := range items {
		key := strings.TrimSpace(it.TKey)
		source := strings.TrimSpace(it.SourceTextTR)
		if key == "" || source == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, UpsertMissingItem{
			TKey:         key,
			SourceTextTR: source,
		})
	}

	return out
}

func normalizeIDListInt64(ids []int64) []int64 {
	seen := make(map[int64]struct{}, len(ids))
	out := make([]int64, 0, len(ids))
	for _, id := range ids {
		if id <= 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out
}

func detectProjectRoot() string {
	wd, err := os.Getwd()
	if err != nil {
		return ""
	}
	candidates := []string{
		wd,
		filepath.Dir(wd),
		filepath.Dir(filepath.Dir(wd)),
	}
	for _, c := range candidates {
		if _, err := os.Stat(filepath.Join(c, "ui")); err == nil {
			return c
		}
	}
	return ""
}

func isCandidateText(s string) bool {
	s = strings.TrimSpace(s)
	if len(s) < 3 || len(s) > 120 {
		return false
	}
	if reBadText.MatchString(s) {
		return false
	}
	if !reHasLetter.MatchString(s) {
		return false
	}
	if strings.Contains(s, "/api/") {
		return false
	}
	if reCodeLikeText.MatchString(s) {
		return false
	}
	if strings.ContainsAny(s, "{}[];`") {
		return false
	}
	symbolCount := 0
	for _, r := range s {
		switch r {
		case '(', ')', '=', ':', '/', '\\', '|', '&', '*', '<', '>', '_':
			symbolCount++
		}
	}
	if symbolCount >= 4 {
		return false
	}
	return true
}

func sanitizeKey(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	s = strings.ReplaceAll(s, " ", "_")
	s = reKeyUnsafe.ReplaceAllString(s, "_")
	s = strings.Trim(s, "_")
	if s == "" {
		return "x"
	}
	return s
}

func normalizeDisplayText(s string) string {
	s = strings.TrimSpace(strings.ReplaceAll(s, "_", " "))
	s = strings.Join(strings.Fields(s), " ")
	if s == "" {
		return ""
	}
	return s
}

func hashKey(s string) string {
	base := sanitizeKey(s)
	if len(base) > 40 {
		base = base[:40]
	}
	sum := 0
	for _, r := range s {
		sum += int(r)
	}
	return fmt.Sprintf("%s_%d", base, sum%1000000)
}

func makeTextBasedSeedKey(sourceText string) string {
	return "txt." + hashKey(normalizeSeedTextKey(sourceText))
}

func normalizeSeedTextKey(s string) string {
	return strings.ToLower(strings.TrimSpace(normalizeDisplayText(s)))
}

func pqArray(values []string) any {
	if len(values) == 0 {
		return pq.Array([]string{})
	}
	out := make([]string, 0, len(values))
	for _, v := range values {
		out = append(out, strings.TrimSpace(v))
	}
	sort.Strings(out)
	return pq.Array(out)
}

func parsePositiveIntEnv(name string, fallback int) int {
	raw := strings.TrimSpace(os.Getenv(name))
	if raw == "" {
		return fallback
	}
	n, err := strconv.Atoi(raw)
	if err != nil || n <= 0 {
		return fallback
	}
	return n
}

func normalizeStringList(items []string, max int) []string {
	if len(items) == 0 {
		return nil
	}
	if max <= 0 {
		max = len(items)
	}
	out := make([]string, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for _, raw := range items {
		v := strings.TrimSpace(raw)
		if v == "" {
			continue
		}
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
		if len(out) >= max {
			break
		}
	}
	return out
}
