import os
import re
import uuid

import requests

from pypos.core.base_service import BaseService
from pypos.core.utils.config_utils import normalize_base_url, read_endpoint_config
from pypos.core.utils.http_json_utils import parse_json_response, sanitize_response_text
from pypos.core.utils.session_context import get_session_manager
from pypos.modules.sinkronisasi.config import (
    get_export_upload_retryable_reasons,
    get_sync_circuit_retryable_statuses,
)

# edited by glg


class ExportUploadApiService(BaseService):
    def __init__(self, http_client=None, retryable_reason_getter=get_export_upload_retryable_reasons):
        super().__init__(http_client=http_client or requests)
        self.retryable_reason_getter = retryable_reason_getter

    # edited by glg
    @staticmethod
    def _new_trace_id(scope: str = "upload") -> str:
        return f"sync-{str(scope or 'upload').strip().lower()}-{uuid.uuid4().hex[:10]}"

    # edited by glg
    @staticmethod
    def _normalize_reason(value, fallback="unexpected_error") -> str:
        raw = str(value or "").strip().lower()
        if not raw:
            raw = str(fallback or "unexpected_error").strip().lower()
        raw = re.sub(r"[^a-z0-9]+", "_", raw).strip("_")
        return raw or "unexpected_error"

    # edited by glg
    @classmethod
    def _normalize_error_code(cls, reason, default_prefix="SYNC_UPLOAD") -> str:
        norm_reason = cls._normalize_reason(reason, "unexpected_error").upper()
        prefix = str(default_prefix or "SYNC_UPLOAD").strip().upper() or "SYNC_UPLOAD"
        return f"{prefix}_{norm_reason}"

    def _build_upload_url(self):
        endpoint_cfg = read_endpoint_config()
        base_url = normalize_base_url(str(endpoint_cfg.get("api_base_url") or ""))
        endpoint = str(endpoint_cfg.get("ep_upload_stream") or "").strip()
        if not base_url:
            raise ValueError("api_base_url belum diatur")
        if not endpoint:
            raise ValueError("Endpoint ep_upload_stream belum diatur")
        if endpoint.startswith(("http://", "https://")):
            return endpoint
        if not endpoint.startswith("/"):
            endpoint = "/" + endpoint
        return f"{base_url}{endpoint}"

    def _build_compile_status_url(self):
        endpoint_cfg = read_endpoint_config()
        base_url = normalize_base_url(str(endpoint_cfg.get("api_base_url") or ""))
        endpoint = str(endpoint_cfg.get("ep_upload_compile_status") or "").strip()
        if not base_url:
            raise ValueError("api_base_url belum diatur")
        if not endpoint:
            raise ValueError("Endpoint ep_upload_compile_status belum diatur")
        if endpoint.startswith(("http://", "https://")):
            return endpoint
        if not endpoint.startswith("/"):
            endpoint = "/" + endpoint
        return f"{base_url}{endpoint}"

    def _build_error_result(self, error, retryable=None, *, error_code="", reason="", trace_id=""):
        status_code = self._extract_status_code(error)
        if retryable is None:
            retryable = self._is_retryable_status(status_code)
            if status_code is None and isinstance(error, (requests.Timeout, requests.ConnectionError)):
                retryable = True
        reason_text = self._normalize_reason(reason or str(error or "upload_error"))
        trace_text = str(trace_id or "").strip() or self._new_trace_id("error")
        error_code_text = str(error_code or "").strip().upper() or self._normalize_error_code(reason_text)
        return {
            "ok": False,
            "error": str(error or "upload_error"),
            "retryable": bool(retryable),
            "status_code": status_code,
            "response_text": "",
            "error_code": error_code_text,
            "reason": reason_text,
            "trace_id": trace_text,
        }

    def _extract_status_code(self, error):
        response = getattr(error, "response", None)
        status_code = getattr(response, "status_code", None)
        try:
            return int(status_code)
        except (TypeError, ValueError):
            return None

    def _is_retryable_status(self, status_code):
        if status_code is None:
            return True
        return int(status_code) in set(get_sync_circuit_retryable_statuses())

    def _is_retryable_reject_message(self, message):
        text = str(message or "").strip().lower()
        if not text:
            return False
        transient_terms = tuple(
            str(v).strip().lower()
            for v in (self.retryable_reason_getter() or [])
            if str(v).strip()
        )
        if not transient_terms:
            return False
        return any(term in text for term in transient_terms)

    def _resolve_upload_mime_type(self, file_name):
        lower_name = str(file_name or "").strip().lower()
        if lower_name.endswith(".xz"):
            return "application/x-xz"
        if lower_name.endswith(".gz"):
            return "application/gzip"
        return "application/octet-stream"

    def upload_file(self, file_path, metadata, timeout):
        trace_id = self._new_trace_id("upload")
        if not file_path or not os.path.isfile(file_path):
            return {
                "ok": False,
                "error": "file_not_found",
                "retryable": False,
                "status_code": None,
                "response_text": "",
                "error_code": "SYNC_UPLOAD_FILE_NOT_FOUND",
                "reason": "file_not_found",
                "trace_id": trace_id,
            }

        try:
            url = self._build_upload_url()
        except ValueError as exc:
            return self._build_error_result(
                exc,
                retryable=False,
                error_code="SYNC_UPLOAD_ENDPOINT_INVALID",
                reason="endpoint_not_ready",
                trace_id=trace_id,
            )
        payload = {str(k): str(v) for k, v in (metadata or {}).items() if k is not None}
        file_name = os.path.basename(file_path)
        mime_type = self._resolve_upload_mime_type(file_name)

        try:
            with open(file_path, "rb") as handle:
                files = {
                    "file_contents": (file_name, handle, mime_type),
                }
                response = self.request_with_retry(
                    "POST",
                    url,
                    data=payload,
                    timeout=int(timeout),
                    retry_on=(requests.RequestException,),
                    files=files,
                    auth_required=True,
                )
        except requests.exceptions.HTTPError as exc:
            return self._build_error_result(exc, trace_id=trace_id, reason="http_error")
        except requests.exceptions.Timeout as exc:
            return self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_UPLOAD_TIMEOUT",
                reason="timeout",
                trace_id=trace_id,
            )
        except requests.exceptions.ConnectionError as exc:
            return self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_UPLOAD_CONNECTION_ERROR",
                reason="connection_error",
                trace_id=trace_id,
            )
        except requests.exceptions.RequestException as exc:
            return self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_UPLOAD_REQUEST_ERROR",
                reason="request_error",
                trace_id=trace_id,
            )
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
            return self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_UPLOAD_UNEXPECTED_ERROR",
                reason="unexpected_error",
                trace_id=trace_id,
            )

        response_text = sanitize_response_text(response.text or "")[:2000]
        try:
            response_json = parse_json_response(response, label="upload_file")
        except (TypeError, ValueError, KeyError, AttributeError):
            response_json = {}

        status_value = None
        if isinstance(response_json, dict):
            status_value = response_json.get("status")

        success_values = {1, "1", True, "true", "ok", "success"}
        is_ok = str(status_value).strip().lower() in {"1", "true", "ok", "success"}
        if status_value in success_values:
            is_ok = True

        if not is_ok:
            message = ""
            if isinstance(response_json, dict):
                message = str(response_json.get("reason") or response_json.get("message") or "").strip()
            if not message:
                message = f"upload_rejected_status_{status_value}"
            retryable = self._is_retryable_reject_message(message)
            return {
                "ok": False,
                "error": message,
                "retryable": retryable,
                "status_code": int(response.status_code),
                "response_text": response_text,
                "error_code": self._normalize_error_code(
                    "upload_rejected_retryable" if retryable else "upload_rejected_non_retryable"
                ),
                "reason": self._normalize_reason(message, "upload_rejected"),
                "trace_id": trace_id,
            }

        return {
            "ok": True,
            "error": "",
            "retryable": False,
            "status_code": int(response.status_code),
            "response_text": response_text,
            "error_code": "",
            "reason": "ok",
            "trace_id": trace_id,
        }

    def fetch_compile_status(self, idempotency_key, timeout):
        trace_id = self._new_trace_id("compile")
        key = str(idempotency_key or "").strip()
        if not key:
            return {
                "ok": False,
                "error": "missing_idempotency_key",
                "retryable": False,
                "status_code": None,
                "response_text": "",
                "response_json": {},
                "error_code": "SYNC_COMPILE_MISSING_IDEMPOTENCY_KEY",
                "reason": "missing_idempotency_key",
                "trace_id": trace_id,
            }

        try:
            url = self._build_compile_status_url()
            params = {"idempotency_key": key}
            session = get_session_manager()
            if session:
                ctx = session.get_machine_context() or {}
                machine_id = str(ctx.get("machine_id") or "").strip()
                if machine_id:
                    params["machine_id"] = machine_id
            response = self.request_with_retry(
                "GET",
                url,
                params=params,
                timeout=int(timeout),
                retry_on=(requests.RequestException,),
                auth_required=True,
            )
        except ValueError as exc:
            result = self._build_error_result(
                exc,
                retryable=False,
                error_code="SYNC_COMPILE_ENDPOINT_INVALID",
                reason="endpoint_not_ready",
                trace_id=trace_id,
            )
            result["response_json"] = {}
            return result
        except requests.exceptions.HTTPError as exc:
            result = self._build_error_result(exc, trace_id=trace_id, reason="http_error")
            result["response_json"] = {}
            return result
        except requests.exceptions.Timeout as exc:
            result = self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_COMPILE_TIMEOUT",
                reason="timeout",
                trace_id=trace_id,
            )
            result["response_json"] = {}
            return result
        except requests.exceptions.ConnectionError as exc:
            result = self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_COMPILE_CONNECTION_ERROR",
                reason="connection_error",
                trace_id=trace_id,
            )
            result["response_json"] = {}
            return result
        except requests.exceptions.RequestException as exc:
            result = self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_COMPILE_REQUEST_ERROR",
                reason="request_error",
                trace_id=trace_id,
            )
            result["response_json"] = {}
            return result
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
            result = self._build_error_result(
                exc,
                retryable=True,
                error_code="SYNC_COMPILE_UNEXPECTED_ERROR",
                reason="unexpected_error",
                trace_id=trace_id,
            )
            result["response_json"] = {}
            return result

        response_text = sanitize_response_text(response.text or "")[:2000]
        try:
            response_json = parse_json_response(response, label="fetch_compile_status")
        except (TypeError, ValueError, KeyError, AttributeError):
            response_json = {}

        status_value = None
        if isinstance(response_json, dict):
            status_value = response_json.get("status")

        success_values = {1, "1", True, "true", "ok", "success"}
        is_ok = str(status_value).strip().lower() in {"1", "true", "ok", "success"}
        if status_value in success_values:
            is_ok = True

        if not is_ok:
            message = ""
            if isinstance(response_json, dict):
                message = str(response_json.get("reason") or response_json.get("message") or "").strip()
            if not message:
                message = f"compile_status_rejected_{status_value}"
            retryable = self._is_retryable_reject_message(message)
            return {
                "ok": False,
                "error": message,
                "retryable": retryable,
                "status_code": int(response.status_code),
                "response_text": response_text,
                "response_json": response_json if isinstance(response_json, dict) else {},
                "error_code": self._normalize_error_code(
                    "compile_rejected_retryable" if retryable else "compile_rejected_non_retryable",
                    default_prefix="SYNC_COMPILE",
                ),
                "reason": self._normalize_reason(message, "compile_status_rejected"),
                "trace_id": trace_id,
            }

        return {
            "ok": True,
            "error": "",
            "retryable": False,
            "status_code": int(response.status_code),
            "response_text": response_text,
            "response_json": response_json if isinstance(response_json, dict) else {},
            "error_code": "",
            "reason": "ok",
            "trace_id": trace_id,
        }
