# #region TranslationExecutor [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, orchestration]
# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch
#         and TranslationRecord rows. Thin orchestrator — delegates to focused sub-services.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationRun]
# @RELATION DEPENDS_ON -> [LLMProviderService], [DictionaryManager], [TranslationPreview]
# @RELATION DEPENDS_ON -> [RunExecutionService], [BatchProcessingService]
# @RELATION DEPENDS_ON -> [LLMTranslationService], [AdaptiveBatchSizer]
# @PRE Valid TranslationRun with job configuration. DB session is available.
# @POST TranslationBatch and TranslationRecord rows are created. Run status is updated.
# @SIDE_EFFECT Calls LLM provider; creates DB rows; updates run statistics.
# @DATA_CONTRACT Input: TranslationRun + Job -> Output: updated Run with batch/record rows
# @INVARIANT Batch processing is independent — one batch failure does not affect others.
# @RATIONALE Extracted from monolithic executor.py (1974 lines) into thin orchestrator
#            to comply with INV_7. Sub-services in _run_service, _batch_proc, _llm_call, _batch_sizer.
#            Module-level helpers moved to _utils.py.
# @REJECTED Keeping monolithic executor.py at 1974 lines — violates INV_7 by +1574 lines.
#           Single monolithic LLM call — would lose all progress on any failure.

from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any

from sqlalchemy.orm import Session

from ...core.config_manager import ConfigManager
from ...core.logger import logger
from ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats
from ...services.llm_provider import LLMProviderService
from ._run_service import RunExecutionService
from ._utils import _check_translation_cache, _compute_source_hash, _enforce_dictionary, estimate_row_tokens

__all__ = [
    "TranslationExecutor",
    "_check_translation_cache",
    "_compute_source_hash",
    "_enforce_dictionary",
    "estimate_row_tokens",
]


class TranslationExecutor:
    """Process translation batches. Thin orchestrator delegating to sub-services."""

    def __init__(
        self, db: Session, config_manager: ConfigManager,
        current_user: str | None = None,
        on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,
    ):
        self.db = db
        self.config_manager = config_manager
        self.current_user = current_user
        self.on_batch_progress = on_batch_progress
        self._preview_edits_cache: dict[str, dict[str, str]] | None = None
        self._run_service = RunExecutionService(db, config_manager, current_user, on_batch_progress)

    # region execute_run [C:3] [TYPE Function] [SEMANTICS translate, run, orchestrate]
    # @BRIEF Run full translation execution. Logic on self for test-patch compatibility.
    # @PRE run is in PENDING or RUNNING status with valid job config.
    # @POST Run is populated with batches and records.
    # @SIDE_EFFECT LLM API calls; DB batch writes.
    def execute_run(
        self, run: TranslationRun,
        llm_progress_callback: Callable[[str, int, int, int], None] | None = None,
        language_stats_map: dict[str, TranslationRunLanguageStats] | None = None,
    ) -> TranslationRun:
        """Execute full translation run. Logic on self for test-patch compatibility."""
        job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()
        if not job:
            raise ValueError(f"Job '{run.job_id}' not found for run '{run.id}'")
        logger.reason("Starting translation execution", {"run_id": run.id, "job_id": job.id})

        run, source_rows, job = self._prepare_run(run, job)
        if run.status != "RUNNING":
            return run

        target_languages, batches = self._prepare_batches(job, source_rows, run.id)
        run.total_records = len(source_rows)
        logger.reason(f"Processing {len(batches)} batches", {"run_id": run.id, "total_rows": run.total_records})

        return self._process_batches(run, job, batches, target_languages, language_stats_map)
    # endregion execute_run

    # region _prepare_run [C:2] [TYPE Function]
    def _prepare_run(self, run: TranslationRun, job: TranslationJob) -> tuple:
        """Prepare run: load preview edits, fetch source rows, filter new keys."""
        self._load_preview_edits(job.id)
        run.status = "RUNNING"
        run.started_at = datetime.now(UTC)
        self.db.flush()

        full_translation = False
        if run.config_snapshot and isinstance(run.config_snapshot, dict):
            full_translation = run.config_snapshot.get("full_translation", False)

        source_rows = self._fetch_source_rows(job.id, run.id)
        if not source_rows:
            logger.explore("No source rows to translate", {"run_id": run.id})
            run.status = "COMPLETED"
            run.completed_at = datetime.now(UTC)
            self.db.flush()
            return run, [], job

        if run.trigger_type == "new_key_only" or (not full_translation and run.trigger_type == "manual"):
            source_rows = self._filter_new_keys(job, run.id, source_rows)
            if not source_rows:
                logger.reason("run_noop — no new rows to translate", {"job_id": job.id, "run_id": run.id})
                run.status = "COMPLETED"
                run.insert_status = "skipped"
                run.total_records = 0
                run.completed_at = datetime.now(UTC)
                self.db.commit()
                return run, [], job
        return run, source_rows, job
    # endregion _prepare_run

    # region _prepare_batches [C:2] [TYPE Function]
    def _prepare_batches(self, job: TranslationJob, source_rows: list, run_id: str) -> tuple:
        """Resolve target languages and create auto-sized batches."""
        target_languages = job.target_languages or [job.target_dialect or "en"]
        if not isinstance(target_languages, list):
            target_languages = [str(target_languages)]
        batches = self._auto_size_batches(job=job, source_rows=source_rows, target_languages=target_languages)
        return target_languages, batches
    # endregion _prepare_batches

    # region _process_batches [C:3] [TYPE Function]
    def _process_batches(self, run: TranslationRun, job: TranslationJob, batches: list,
                         target_languages: list[str],
                         language_stats_map: dict[str, TranslationRunLanguageStats] | None = None) -> TranslationRun:
        """Process all batches: execute, insert to target, check cancellation."""
        successful_records = failed_records = skipped_records = 0
        for batch_idx, batch_rows in enumerate(batches):
            batch_result = self._process_batch(
                job=job, run_id=run.id, batch_index=batch_idx, batch_rows=batch_rows,
                dict_snapshot_hash=run.dict_snapshot_hash, config_hash=run.config_hash,
            )
            successful_records += batch_result.get("successful", 0)
            failed_records += batch_result.get("failed", 0)
            skipped_records += batch_result.get("skipped", 0)
            run.successful_records = successful_records
            run.failed_records = failed_records
            run.skipped_records = skipped_records
            self.db.commit()

            batch_id = batch_result.get("batch_id")
            if batch_id and batch_result.get("successful", 0) > 0:
                try:
                    self._insert_batch_to_target(job, batch_id, run.id)
                except Exception as e:
                    logger.explore("Batch INSERT failed (non-fatal)", {"batch_id": batch_id, "error": str(e)})

            self.db.refresh(run)
            if run.error_message == "CANCEL_REQUESTED":
                logger.reason("Cancellation requested — stopping", {"run_id": run.id, "batch_index": batch_idx})
                run.status = "CANCELLED"
                run.completed_at = datetime.now(UTC)
                run.error_message = None
                self.db.commit()
                return run

            if language_stats_map and batch_result.get("successful", 0) > 0:
                try:
                    self._update_language_stats_incremental(run.id, language_stats_map)
                except Exception as e:
                    logger.explore("Language stats update failed (non-fatal)", {"batch_id": batch_id, "error": str(e)})
            if self.on_batch_progress:
                self.on_batch_progress(run.id, batch_idx + 1, len(batches), successful_records, run.total_records or 0)

        return self._finalize_run(run, successful_records, failed_records, skipped_records)
    # endregion _process_batches

    @staticmethod
    def _finalize_run(run: TranslationRun, s: int, f: int, sk: int) -> TranslationRun:
        if f == 0 and sk == 0:
            run.status = "COMPLETED"
        elif s == 0:
            run.status = "FAILED"
            run.error_message = f"All {f} record(s) failed — see individual record errors for details"
        else:
            run.status = "COMPLETED"
        run.completed_at = datetime.now(UTC)
        logger.reflect("Translation execution complete", {"run_id": run.id, "status": run.status,
                       "successful": s, "failed": f, "skipped": sk})
        return run

    # -- Delegation methods (thin wrappers for test-patch compatibility) --
    def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:
        return self._run_service._fetch_source_rows(job_id, run_id)

    def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:
        return self._run_service._filter_new_keys(job, run_id, source_rows)

    @staticmethod
    def _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:
        return RunExecutionService._extract_chart_data_rows(response)

    def _load_preview_edits(self, job_id: str) -> None:
        self._run_service._load_preview_edits(job_id)
        self._preview_edits_cache = self._run_service._preview_edits_cache

    @staticmethod
    def _compute_key_hash(source_data: dict) -> str:
        return RunExecutionService._compute_key_hash(source_data)

    def _update_language_stats_incremental(self, run_id: str, language_stats_map) -> None:
        self._run_service._update_language_stats_incremental(run_id, language_stats_map)

    def _resolve_provider_model(self, job) -> str | None:
        """Resolve the LLM provider model name for token budget estimation."""
        if not job.provider_id:
            return None
        try:
            p_svc = LLMProviderService(self.db)
            p = p_svc.get_provider(job.provider_id)
            if p:
                return p.default_model or "gpt-4o-mini"
        except Exception:
            pass
        return None

    def _auto_size_batches(self, job, source_rows, target_languages, provider_info=None) -> list:
        """Split source rows into auto-sized batches based on content length."""
        from ._batch_sizer import AdaptiveBatchSizer
        if provider_info is None:
            provider_info = self._resolve_provider_model(job)
        return AdaptiveBatchSizer(self.db, self.config_manager).auto_size_batches(
            job, source_rows, target_languages, provider_info)

    def _process_batch(self, job, run_id, batch_index, batch_rows, dict_snapshot_hash=None, config_hash=None) -> dict:
        from ._batch_proc import BatchProcessingService
        return BatchProcessingService(self.db, self.config_manager).process_batch(
            job, run_id, batch_index, batch_rows, dict_snapshot_hash, config_hash,
            preview_edits_cache=self._preview_edits_cache)

    def _insert_batch_to_target(self, job, batch_id, run_id) -> None:
        from ._batch_proc import BatchProcessingService
        BatchProcessingService(self.db, self.config_manager).insert_batch_to_target(job, batch_id, run_id)

    def _call_llm_for_batch(self, job, run_id, batch_rows, dict_matches, batch_id, max_tokens=8192, _recursion_depth=0) -> dict:
        from ._llm_call import LLMTranslationService
        return LLMTranslationService(self.db).call_llm_for_batch(
            job, run_id, batch_rows, dict_matches, batch_id, max_tokens, _recursion_depth)

    def _call_llm(self, job, prompt, max_tokens=8192) -> tuple:
        from ._llm_call import LLMTranslationService
        return LLMTranslationService(self.db).call_llm(job, prompt, max_tokens)

    @staticmethod
    def _call_openai_compatible(base_url, api_key, model, prompt, provider_type="openai", max_tokens=8192, disable_reasoning=False) -> tuple:
        from ._llm_call import LLMTranslationService
        return LLMTranslationService.call_openai_compatible(base_url, api_key, model, prompt, provider_type, max_tokens, disable_reasoning)

    @staticmethod
    def _parse_llm_response(response_text, expected_count, target_languages=None, finish_reason=None) -> dict:
        from ._llm_call import LLMTranslationService
        return LLMTranslationService._parse_llm_response(response_text, expected_count, target_languages, finish_reason)
# #endregion TranslationExecutor
