# #region TranslationPreview [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, preview, llm, review]
# @BRIEF Preview session management: fetch sample rows from Superset, send to LLM with context + filtered dictionary, return side-by-side results.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TranslationJob]
# @RELATION DEPENDS_ON -> [TranslationPreviewSession]
# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]
# @RELATION DEPENDS_ON -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [DictionaryManager]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [LanguageDetectService]
# @RELATION DEPENDS_ON -> [render_prompt]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @PRE Database session and config manager are available.
# @POST Preview sessions are created with LLM-translated rows; records can be approved/edited/rejected.
# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates DB rows.
# @RATIONALE C4 because preview is stateful with approve/edit/reject lifecycle and LLM API calls.
# @REJECTED Transient preview state (in-memory/session-scoped) — would lose decisions on restart and cannot gate execution reliably.

from datetime import UTC, datetime, timedelta
import time
from typing import Any
import uuid

from sqlalchemy.orm import Session

from ...core.config_manager import ConfigManager
from ...core.logger import belief_scope, logger
from ...models.translate import (
    TranslationJob,
    TranslationPreviewLanguage,
    TranslationPreviewRecord,
    TranslationPreviewSession,
)
from ._lang_detect import detect_language, get_detector
from .preview_constants import DEFAULT_EXECUTION_PROMPT_TEMPLATE, DEFAULT_PREVIEW_PROMPT_TEMPLATE  # noqa: F401
from .preview_executor import PreviewExecutor
from .preview_prompt_builder import PreviewPromptBuilder
from .preview_response_parser import (
    compute_config_hash as _compute_config_hash_module,
    parse_llm_response as _parse_llm_response_module,
)
from .preview_review import PreviewSessionManager


# #region TranslationPreview [C:4] [TYPE Class]
# @BRIEF Manages preview lifecycle: fetch sample rows, call LLM, manage row-level approve/edit/reject, accept gate.
# @PRE Database session and config manager are available.
# @POST Preview sessions created with persisted records; full execution gates on accepted session.
# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates DB rows.
class TranslationPreview:

    def __init__(
        self,
        db: Session,
        config_manager: ConfigManager,
        current_user: str | None = None,
    ):
        self.db = db
        self.config_manager = config_manager
        self.current_user = current_user
        self._executor = PreviewExecutor(db, config_manager)
        self._prompt_builder = PreviewPromptBuilder(db)
        self._session_mgr = PreviewSessionManager(db)

    # region preview_rows [TYPE Function]
    # @PURPOSE: Fetch sample rows, send to LLM, create preview session with per-language records.
    # @SIDE_EFFECT  Fetches data from Superset; calls LLM; creates DB rows.
    def preview_rows(
        self,
        job_id: str,
        sample_size: int = 10,
        prompt_template: str | None = None,
        env_id: str | None = None,
    ) -> dict[str, Any]:
        with belief_scope("TranslationPreview.preview_rows"):
            job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()
            if not job:
                raise ValueError(f"Translation job '{job_id}' not found")
            if not job.source_datasource_id:
                raise ValueError("Job must have a source datasource configured for preview")
            if not job.translation_column:
                raise ValueError("Job must have a translation column configured for preview")
            if not job.target_languages:
                raise ValueError("Job must have at least one target language configured for preview")
            if not job.provider_id:
                raise ValueError("Job must have an LLM provider configured for preview")

            target_languages = job.target_languages or [job.target_dialect or "en"]
            if not isinstance(target_languages, list):
                target_languages = [str(target_languages)]

            config_hash = self._executor.compute_config_hash(job)
            dict_snapshot_hash = self._executor.compute_dict_snapshot_hash(job_id)
            source_rows = self._executor.fetch_sample_rows(job=job, sample_size=sample_size, env_id=env_id)
            if not source_rows:
                raise ValueError("No rows returned from datasource for preview")

            provider_model = self._executor.resolve_provider_model(job)
            budget_result = self._prompt_builder.estimate_token_budget_for_rows(
                source_rows=source_rows, target_languages=target_languages, job=job, provider_model=provider_model,
            )
            source_rows, actual_row_count, token_budget = (
                budget_result["source_rows"], budget_result["actual_row_count"], budget_result["token_budget"],
            )

            if token_budget.get("warning"):
                logger.explore("Token budget warning", {"warning": token_budget["warning"], "sample_size": actual_row_count})

            prompt_data = self._prompt_builder.build_prompt_from_rows(
                job=job, source_rows=source_rows, sample_size=sample_size, prompt_template=prompt_template,
            )

            # ★ NEW: Run local language detection on row_meta (no LLM)
            detector = get_detector(target_languages)
            for meta in prompt_data["row_meta"]:
                meta["_detected_lang"] = detect_language(meta["source_text"], detector)

            t_llm = time.monotonic()
            llm_response = self._executor.call_llm(
                job=job, prompt=prompt_data["prompt"], max_tokens=token_budget["max_output_needed"],
            )
            logger.reason(f"TIMING: LLM call: {time.monotonic() - t_llm:.2f}s", {})

            translations = self._executor.parse_llm_response(
                llm_response, prompt_data["actual_row_count"], target_languages=target_languages,
            )

            session = TranslationPreviewSession(
                id=str(uuid.uuid4()), job_id=job_id, status="ACTIVE",
                created_by=self.current_user, created_at=datetime.now(UTC),
                expires_at=datetime.now(UTC) + timedelta(hours=24),
            )
            self.db.add(session)
            self.db.flush()

            records = self._create_preview_records(
                job=job, row_meta=prompt_data["row_meta"], translations=translations,
                target_languages=target_languages, session=session,
            )

            result = {
                "id": session.id, "job_id": job_id, "status": "ACTIVE",
                "created_by": self.current_user, "created_at": session.created_at.isoformat(),
                "expires_at": session.expires_at.isoformat() if session.expires_at else None,
                "records": records, "target_languages": target_languages,
                "cost_estimate": {
                    "sample_size": prompt_data["actual_row_count"],
                    "num_languages": prompt_data["num_languages"],
                    "sample_prompt_tokens": prompt_data["sample_prompt_tokens"],
                    "sample_output_tokens": prompt_data["sample_output_tokens"],
                    "sample_total_tokens": prompt_data["sample_total_tokens"],
                    "sample_cost": prompt_data["sample_cost"],
                    "estimated_total_rows": prompt_data["total_est_rows"],
                    "estimated_tokens": prompt_data["total_est_tokens"],
                    "estimated_cost": prompt_data["total_est_cost"],
                    "warning": prompt_data["cost_warning"],
                },
                "token_budget": {
                    "batch_size_adjusted": token_budget["batch_size_adjusted"],
                    "estimated_input_tokens": token_budget["estimated_input_tokens"],
                    "estimated_output_tokens": token_budget["estimated_output_tokens"],
                    "max_output_needed": token_budget["max_output_needed"],
                    "warning": token_budget["warning"],
                },
                "config_hash": config_hash, "dict_snapshot_hash": dict_snapshot_hash,
            }
            logger.reflect("Preview completed", {"session_id": session.id, "row_count": actual_row_count})
            return result
    # endregion preview_rows

    # region _create_preview_records [TYPE Function]
    def _create_preview_records(
        self,
        job: TranslationJob, row_meta: list[dict[str, Any]],
        translations: dict[str, dict[str, str]], target_languages: list[str],
        session: TranslationPreviewSession,
    ) -> list[dict[str, Any]]:
        records = []
        for meta in row_meta:
            idx = meta["row_index"]
            source_text = meta["source_text"]
            translation_data = translations.get(str(idx), {})
            # ★ Local detection (from lingua) takes priority over LLM response
            detected_lang = meta.get("_detected_lang", "und") or "und"
            if detected_lang == "und" and isinstance(translation_data, dict):
                detected_lang = translation_data.get("detected_source_language", "und") or "und"

            lang_entries: list[TranslationPreviewLanguage] = []
            for lang_code in target_languages:
                lang_translation = None
                if isinstance(translation_data, dict):
                    lang_translation = translation_data.get(lang_code) or translation_data.get("translation", "")
                elif isinstance(translation_data, str):
                    lang_translation = translation_data
                if str(lang_code).lower() == str(detected_lang).lower() and detected_lang != "und":
                    lang_translation = source_text
                if not lang_translation:
                    lang_translation = ""

                lang_entries.append(TranslationPreviewLanguage(
                    id=str(uuid.uuid4()), preview_record_id="",
                    language_code=lang_code, source_language_detected=detected_lang,
                    translated_value=str(lang_translation), final_value=str(lang_translation),
                    status="pending", needs_review=(detected_lang == "und"),
                ))

            source_row = meta.get("source_row", {})
            source_data = None
            if job.target_key_cols:
                source_data = {k: source_row.get(k) for k in job.target_key_cols if k in source_row}
            elif source_row:
                source_data = dict(source_row)

            record = TranslationPreviewRecord(
                id=str(uuid.uuid4()), session_id=session.id, source_sql=source_text,
                target_sql=lang_entries[0].translated_value if lang_entries else "",
                source_object_type="table_row", source_object_id=str(idx),
                source_object_name=f"Row {idx + 1}", source_data=source_data,
                status="PENDING", feedback=None, created_at=datetime.now(UTC),
            )
            self.db.add(record)
            self.db.flush()

            serialized_langs = []
            for le in lang_entries:
                le.preview_record_id = record.id
                self.db.add(le)
                serialized_langs.append({
                    "language_code": le.language_code, "source_language_detected": le.source_language_detected,
                    "translated_value": le.translated_value, "final_value": le.final_value,
                    "status": le.status, "needs_review": le.needs_review,
                })

            records.append({
                "id": record.id, "source_sql": record.source_sql, "target_sql": record.target_sql,
                "source_object_type": record.source_object_type, "source_object_id": record.source_object_id,
                "source_object_name": record.source_object_name, "status": record.status,
                "feedback": record.feedback, "source_language_detected": detected_lang,
                "needs_review": any(le.needs_review for le in lang_entries),
                "languages": serialized_langs,
            })

        self.db.commit()
        return records
    # endregion _create_preview_records

    # Backward-compatible static methods (delegates to preview_response_parser)
    @staticmethod
    def _parse_llm_response(
        response_text: str, expected_count: int,
        target_languages: list[str] | None = None,
        finish_reason: str | None = None,
    ) -> dict[str, dict[str, str]]:
        return _parse_llm_response_module(response_text, expected_count, target_languages, finish_reason)

    @staticmethod
    def _compute_config_hash(job: TranslationJob) -> str:
        return _compute_config_hash_module(job)

    # Delegated methods
    def update_preview_row(self, job_id: str, row_id: str, action: str, translation: str | None = None,
                           feedback: str | None = None, language_code: str | None = None) -> dict[str, Any]:
        return self._session_mgr.update_preview_row(job_id, row_id, action, translation, feedback, language_code)

    def accept_preview_session(self, job_id: str) -> dict[str, Any]:
        return self._session_mgr.accept_preview_session(job_id)

    def get_preview_session(self, job_id: str) -> dict[str, Any]:
        return self._session_mgr.get_preview_session(job_id)

# #endregion TranslationPreview

# Re-export for backward compatibility
# #endregion TranslationPreview
