﻿# edited by glg
from datetime import datetime
import sqlite3
import uuid

from pypos.modules.penjualan.errors import TransaksiSaveError
from pypos.modules.penjualan.services.error_envelope_service import ErrorEnvelopeService
from pypos.modules.penjualan.services.transaksi_enterprise_control_service import (
    TransaksiEnterpriseControlService,
)


class _NoopEnterpriseControlService:
    def acquire_idempotency_lock(self, **_kwargs):
        return {"proceed": True, "state": "NOOP"}

    def mark_idempotency_success(self, **_kwargs):
        return None

    def mark_idempotency_failed(self, **_kwargs):
        return None

    def append_persist_audit(self, **_kwargs):
        return 0

    @staticmethod
    def build_payload_hash(**_kwargs):
        return ""


class TransaksiPersistFlowService:
    """
    Orkestrasi simpan transaksi agar controller fokus pada alur UI.
    """

    def __init__(self, transaksi_save_service, enterprise_control_service=None):
        self.transaksi_save_service = transaksi_save_service
        if enterprise_control_service is not None:
            self.enterprise_control_service = enterprise_control_service
            return
        db_path = str(getattr(transaksi_save_service, "db_path", "") or "").strip()
        if db_path:
            self.enterprise_control_service = TransaksiEnterpriseControlService(db_path=db_path)
        else:
            self.enterprise_control_service = _NoopEnterpriseControlService()

    @staticmethod
    def _generate_trace_id(scope: str = "trx_save") -> str:
        suffix = uuid.uuid4().hex[:8]
        stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")[-12:]
        return f"{str(scope or 'trx_save')}-{stamp}-{suffix}"

    @staticmethod
    def build_free_items_summary(arr_free_produk):
        free_map = {}
        for item in list(arr_free_produk or []):
            row = item if isinstance(item, dict) else {}
            try:
                qty = int(row.get("free_qty", 0) or 0)
            except (TypeError, ValueError):
                qty = 0
            if qty <= 0:
                continue
            free_name = str(row.get("free_produk_nama", "") or "").strip()
            if not free_name:
                continue
            free_map[free_name] = int(free_map.get(free_name, 0)) + int(qty)
        if not free_map:
            return []
        return [{"nama": nama, "qty": qty} for nama, qty in sorted(free_map.items())]

    @staticmethod
    def _resolve_idempotency_key(transaksi_data_dict):
        payload = transaksi_data_dict if isinstance(transaksi_data_dict, dict) else {}
        key = str(payload.get("idempotency_key") or "").strip()
        if key:
            return key
        nomer = str(payload.get("nomer") or "").strip()
        if nomer:
            key = f"trx-{nomer.lower()}"
        else:
            key = f"trx-save-{uuid.uuid4().hex[:16]}"
        payload["idempotency_key"] = key
        return key

    def _build_error_envelope(self, *, trace_id, error_code, reason, message, exc):
        return ErrorEnvelopeService.build_error(
            status=0,
            error_code=error_code,
            reason=reason,
            message=message,
            trace_id=trace_id,
            payload={"error": str(exc)},
            code_prefix="TRX",
        )

    @staticmethod
    def _resolve_save_reason(error_code: str, fallback: str = "transaksi_save_error") -> str:
        code = str(error_code or "").strip().upper()
        mapping = {
            "TRX_SAVE_DB_ERROR": "db_error",
            "TRX_SAVE_F9_DB_ERROR": "db_error",
            "TRX_SAVE_DATA_ERROR": "invalid_transaksi_data",
            "TRX_SAVE_F9_DATA_ERROR": "invalid_transaksi_data",
            "TRX_SAVE_UNEXPECTED_ERROR": "runtime_error",
            "TRX_SAVE_F9_UNEXPECTED_ERROR": "runtime_error",
        }
        if code in mapping:
            return mapping[code]
        return str(fallback or "transaksi_save_error")

    def _record_failure(self, *, idempotency_key, trace_id, error):
        error_payload = error if isinstance(error, dict) else {}
        self.enterprise_control_service.mark_idempotency_failed(
            idempotency_key=idempotency_key,
            error_code=str(error_payload.get("error_code") or "TRX_SAVE_ERROR"),
            reason=str(error_payload.get("reason") or "transaksi_save_error"),
            trace_id=trace_id,
        )
        self.enterprise_control_service.append_persist_audit(
            event_type="FAILED",
            status="error",
            idempotency_key=idempotency_key,
            trace_id=trace_id,
            error_code=str(error_payload.get("error_code") or ""),
            reason=str(error_payload.get("reason") or ""),
            payload=error_payload,
        )

    def _build_duplicate_success_response(self, *, transaksi_id, trace_id, idempotency_key):
        return {
            "ok": True,
            "transaksi_id": int(transaksi_id or 0),
            "arr_free_produk": [],
            "free_items_summary": [],
            "trace_id": trace_id,
            "idempotency_key": idempotency_key,
            "idempotent_replay": True,
        }

    def _handle_idempotency_precheck(
        self,
        *,
        idempotency_key,
        payload_hash,
        trace_id,
    ):
        lock_state = self.enterprise_control_service.acquire_idempotency_lock(
            idempotency_key=idempotency_key,
            payload_hash=payload_hash,
            trace_id=trace_id,
        )
        if bool(lock_state.get("proceed")):
            return {"ok": True, "lock_state": lock_state}

        state = str(lock_state.get("state") or "").upper()
        if state == "DUPLICATE_SUCCESS":
            return {
                "ok": False,
                "result": self._build_duplicate_success_response(
                    transaksi_id=lock_state.get("transaksi_id"),
                    trace_id=trace_id,
                    idempotency_key=idempotency_key,
                ),
            }

        reason = str(lock_state.get("reason") or "idempotency_reject")
        error_code = str(lock_state.get("error_code") or "TRX_IDEMPOTENCY_REJECT")
        error = self._build_error_envelope(
            trace_id=trace_id,
            error_code=error_code,
            reason=reason,
            message="Permintaan transaksi sedang diproses atau bentrok idempotency key.",
            exc=reason,
        )
        self._record_failure(
            idempotency_key=idempotency_key,
            trace_id=trace_id,
            error=error,
        )
        return {"ok": False, "result": {"ok": False, "error": error}}

    def _build_persist_context(self, *, transaksi_data, detail_data, transaksi_data_dict, audit_message):
        trace_id = self._generate_trace_id("trx_save")
        payload = transaksi_data_dict if isinstance(transaksi_data_dict, dict) else {}
        if payload:
            payload["trace_id"] = trace_id
        idempotency_key = self._resolve_idempotency_key(transaksi_data_dict)
        payload_hash = self.enterprise_control_service.build_payload_hash(
            transaksi_data=transaksi_data,
            detail_data=detail_data,
            transaksi_data_dict=transaksi_data_dict,
            audit_message=audit_message,
        )
        return {
            "trace_id": trace_id,
            "idempotency_key": idempotency_key,
            "payload_hash": payload_hash,
        }

    def _run_idempotency_precheck(self, context):
        precheck = self._handle_idempotency_precheck(
            idempotency_key=context["idempotency_key"],
            payload_hash=context["payload_hash"],
            trace_id=context["trace_id"],
        )
        if not bool(precheck.get("ok")):
            return precheck.get("result") or {"ok": False}
        self.enterprise_control_service.append_persist_audit(
            event_type="STARTED",
            status="processing",
            idempotency_key=context["idempotency_key"],
            trace_id=context["trace_id"],
            payload={"audit_message": str(context.get("audit_message") or "")},
        )
        return None

    def _build_success_result(self, *, context, transaksi_id, arr_free_produk):
        self.enterprise_control_service.mark_idempotency_success(
            idempotency_key=context["idempotency_key"],
            transaksi_id=transaksi_id,
            trace_id=context["trace_id"],
        )
        self.enterprise_control_service.append_persist_audit(
            event_type="SUCCESS",
            status="ok",
            idempotency_key=context["idempotency_key"],
            trace_id=context["trace_id"],
            transaksi_id=int(transaksi_id or 0),
            payload={"free_items_count": len(list(arr_free_produk or []))},
        )
        return {
            "ok": True,
            "transaksi_id": transaksi_id,
            "arr_free_produk": list(arr_free_produk or []),
            "free_items_summary": self.build_free_items_summary(arr_free_produk),
            "trace_id": context["trace_id"],
            "idempotency_key": context["idempotency_key"],
        }

    def _map_persist_exception_to_error(self, *, context, exc):
        if isinstance(exc, TransaksiSaveError):
            error_code = str(getattr(exc, "code", "TRX_SAVE_ERROR") or "TRX_SAVE_ERROR")
            error_message = str(getattr(exc, "message", "") or "").strip() or "Gagal menyimpan transaksi."
            return self._build_error_envelope(
                trace_id=context["trace_id"],
                error_code=error_code,
                reason=self._resolve_save_reason(error_code, fallback="transaksi_save_error"),
                message=error_message,
                exc=exc,
            )
        if isinstance(exc, sqlite3.Error):
            return self._build_error_envelope(
                trace_id=context["trace_id"],
                error_code="TRX_SAVE_SQLITE_ERROR",
                reason="sqlite_error",
                message="Gagal menyimpan transaksi ke database lokal.",
                exc=exc,
            )
        if isinstance(exc, (TypeError, ValueError, KeyError)):
            return self._build_error_envelope(
                trace_id=context["trace_id"],
                error_code="TRX_SAVE_CONTROLLER_DATA_ERROR",
                reason="invalid_transaksi_data",
                message="Data transaksi tidak valid.",
                exc=exc,
            )
        return self._build_error_envelope(
            trace_id=context["trace_id"],
            error_code="TRX_SAVE_RUNTIME_ERROR",
            reason="runtime_error",
            message="Gagal menyimpan transaksi. Silakan coba lagi.",
            exc=exc,
        )

    def _save_with_error_mapping(
        self,
        *,
        context,
        transaksi_data,
        detail_data,
        transaksi_data_dict,
        audit_message,
        voucher_callback,
    ):
        try:
            transaksi_id, arr_free_produk = self.transaksi_save_service.save_with_audit(
                transaksi_data=transaksi_data,
                detail_data=detail_data,
                transaksi_data_dict=transaksi_data_dict,
                audit_message=audit_message,
                voucher_callback=voucher_callback,
                trace_id=context["trace_id"],
            )
            return self._build_success_result(
                context=context,
                transaksi_id=transaksi_id,
                arr_free_produk=arr_free_produk,
            )
        except (TransaksiSaveError, sqlite3.Error, TypeError, ValueError, KeyError, RuntimeError) as exc:
            error = self._map_persist_exception_to_error(context=context, exc=exc)
        self._record_failure(
            idempotency_key=context["idempotency_key"],
            trace_id=context["trace_id"],
            error=error,
        )
        return {"ok": False, "error": error}

    def persist(
        self,
        *,
        transaksi_data,
        detail_data,
        transaksi_data_dict,
        audit_message,
        voucher_callback=None,
    ):
        context = self._build_persist_context(
            transaksi_data=transaksi_data,
            detail_data=detail_data,
            transaksi_data_dict=transaksi_data_dict,
            audit_message=audit_message,
        )
        context["audit_message"] = str(audit_message or "")
        precheck_result = self._run_idempotency_precheck(context)
        if precheck_result is not None:
            return precheck_result
        return self._save_with_error_mapping(
            context=context,
            transaksi_data=transaksi_data,
            detail_data=detail_data,
            transaksi_data_dict=transaksi_data_dict,
            audit_message=audit_message,
            voucher_callback=voucher_callback,
        )
