import platform
import socket
import uuid
import logging
import sqlite3

from pypos.core.base_service import BaseService
from pypos.core.utils.app_state_utils import set_bool
from pypos.core.utils.cabang_utils import get_branch_rows
from pypos.core.utils.config_utils import normalize_base_url, read_config, read_endpoint_config, save_config, save_endpoint_config
from pypos.core.utils.db_helper import connect_sqlite
from pypos.core.utils.sql_query_builder import build_insert_sql, render_sql_template
from pypos.core.utils.device_utils import (
    cek_device_ke_server,
    get_device_id,
    is_device_not_registered,
    post_device_registration,
    simpan_device_lokal,
    simpan_device_pending_lokal,
)
from pypos.core.utils.ui_message_utils import sanitize_ui_message
from pypos.modules.auth.services.device_registration_request_builder import (
    DeviceRegistrationRequestBuilder,
)
from pypos.modules.auth.services.device_registration_response_mapper import (
    DeviceRegistrationResponseMapper,
)
from pypos.modules.auth.services.device_registration_retry_policy import (
    DeviceRegistrationRetryPolicy,
)

LOGGER = logging.getLogger(__name__)
_NUMERIC_PARSE_EXCEPTIONS = (TypeError, ValueError, OverflowError)
_CONFIG_WRITE_EXCEPTIONS = (OSError, RuntimeError, ValueError, TypeError)
_DB_OPERATION_EXCEPTIONS = (sqlite3.Error, OSError, RuntimeError, ValueError, TypeError)
_DEVICE_RESOLVE_EXCEPTIONS = (RuntimeError, OSError, ValueError, TypeError)

# edited by glg


class DeviceRegistrationService(BaseService):
    def __init__(self):
        super().__init__()
        self.device_registration_request_builder = DeviceRegistrationRequestBuilder()
        self.device_registration_response_mapper = DeviceRegistrationResponseMapper()
        self.device_registration_retry_policy = DeviceRegistrationRetryPolicy()

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

    # edited by glg
    def _build_error_result(self, *, level: str, message: str, error_code: str, reason: str, trace_id: str = ""):
        return {
            "ok": False,
            "level": str(level or "warning"),
            "message": str(message or "").strip(),
            "error_code": str(error_code or "AUTH_DEVICE_REG_ERROR").strip().upper(),
            "reason": str(reason or "device_registration_error").strip().lower(),
            "trace_id": str(trace_id or "").strip() or self._new_trace_id("error"),
        }

    # edited by glg
    def _get_request_builder(self) -> DeviceRegistrationRequestBuilder:
        service = getattr(self, "device_registration_request_builder", None)
        if service is None:
            service = DeviceRegistrationRequestBuilder()
            self.device_registration_request_builder = service
        return service

    # edited by glg
    def _get_response_mapper(self) -> DeviceRegistrationResponseMapper:
        service = getattr(self, "device_registration_response_mapper", None)
        if service is None:
            service = DeviceRegistrationResponseMapper()
            self.device_registration_response_mapper = service
        return service

    # edited by glg
    def _get_retry_policy(self) -> DeviceRegistrationRetryPolicy:
        service = getattr(self, "device_registration_retry_policy", None)
        if service is None:
            service = DeviceRegistrationRetryPolicy()
            self.device_registration_retry_policy = service
        return service

    def _parse_toko_id(self, toko_id):
        return self._get_request_builder().parse_toko_id(toko_id)

    def load_branches(self, toko_id=None):
        filters = []
        params = []
        if toko_id is not None and str(toko_id).strip():
            try:
                toko_id_int = self._parse_toko_id(toko_id)
                filters.append("CAST(COALESCE(toko_id, '') AS TEXT) = ?")
                params.append(str(toko_id_int))
            except ValueError:
                return []

        where_clause = "WHERE COALESCE(trash, 0) = 0 AND COALESCE(status, 1) = 1"
        if filters:
            where_clause += " AND " + " AND ".join(filters)

        conn = None
        branches = []
        try:
            conn = connect_sqlite()
            cur = conn.cursor()
            query = render_sql_template(
                "SELECT nama, id FROM per_cabang {where_clause} ORDER BY nama ASC",
                where_clause=where_clause,
            )
            cur.execute(
                query,
                params,
            )
            branches = cur.fetchall() or []
            cur.close()
        except _DB_OPERATION_EXCEPTIONS as e:
            LOGGER.warning("Gagal load cabang lokal: %s", e)
            branches = []
        finally:
            if conn is not None:
                conn.close()

        filtered = []
        for name, branch_id in branches:
            # edited by glg
            # Tampilkan cabang dinamis sesuai data endpoint (termasuk id negatif seperti pusat=-1).
            # Validasi hanya id tidak kosong agar dropdown tetap bersih.
            branch_id_text = str(branch_id or "").strip()
            if not branch_id_text:
                continue
            filtered.append((str(name), branch_id_text))
        return filtered

    # edited by glg
    def _build_branch_options_from_rows(self, branch_rows):
        options = []
        for row in branch_rows or []:
            if not isinstance(row, dict):
                continue
            name = str(row.get("nama") or "").strip()
            branch_id_text = str(row.get("id") or "").strip()
            if not name or not branch_id_text:
                continue
            options.append((name, branch_id_text))
        options.sort(key=lambda item: item[0].lower())
        return options

    def _replace_branches_for_toko(self, toko_id, branch_rows):
        conn = connect_sqlite()
        try:
            cur = conn.cursor()
            cur.execute("DELETE FROM per_cabang WHERE CAST(COALESCE(toko_id, '') AS TEXT) = ?", (str(toko_id),))
            # edited by glg
            # Dukung kolom gudang default per_cabang bila schema lokal sudah memilikinya.
            cur.execute("PRAGMA table_info(per_cabang)")
            local_cols = {
                str(row[1]).strip() for row in (cur.fetchall() or [])
                if row and len(row) > 1 and row[1]
            }
            insert_columns = [
                "id",
                "last_update",
                "nama",
                "kelurahan",
                "kecamatan",
                "kabupaten",
                "propinsi",
                "alamat",
                "toko_id",
                "status",
                "trash",
                "jenis",
                "harga_jenis",
                "point_jenis",
                "dtime",
                "creator_nama",
                "opname_jenis",
            ]
            optional_column_candidates = {
                "gudang_id": ("gudang_id", "gudangID", "default_gudang_id", "id_gudang"),
                "gudang_nama": ("gudang_nama", "gudangName", "default_gudang_nama", "nama_gudang"),
            }
            for optional_col in ("gudang_id", "gudang_nama"):
                if optional_col in local_cols:
                    insert_columns.append(optional_col)

            insert_sql = build_insert_sql("per_cabang", insert_columns)

            def _pick_value(row_map, keys, default_value=""):
                for key in keys:
                    if key in row_map and row_map.get(key) not in (None, ""):
                        return row_map.get(key)
                return default_value

            # edited by glg
            # Fallback gudang per cabang:
            # jika server belum kirim mapping gudang_id, isi default cabang agar
            # semua POS dalam cabang memakai gudang yang sama.
            def _fallback_gudang_id_from_branch_id(branch_id_value):
                try:
                    cabang_id_int = int(str(branch_id_value).strip())
                except _NUMERIC_PARSE_EXCEPTIONS:
                    return 0
                if cabang_id_int <= 0:
                    return 0
                return int(cabang_id_int * -10)

            for row in branch_rows:
                values_by_column = {
                    "id": row.get("id"),
                    "last_update": row.get("last_update") or row.get("dtime") or "",
                    "nama": row.get("nama") or "",
                    "kelurahan": row.get("kelurahan") or "",
                    "kecamatan": row.get("kecamatan") or "",
                    "kabupaten": row.get("kabupaten") or "",
                    "propinsi": row.get("propinsi") or "",
                    "alamat": row.get("alamat") or row.get("address") or "",
                    "toko_id": str(row.get("toko_id") or ""),
                    # edited by glg
                    # status=0 dari endpoint harus tetap tersimpan sebagai nonaktif.
                    "status": int(1 if row.get("status") in (None, "") else row.get("status")),
                    "trash": int(row.get("trash") or 0),
                    "jenis": row.get("jenis") or "",
                    "harga_jenis": row.get("harga_jenis") or "",
                    "point_jenis": row.get("point_jenis") or "",
                    "dtime": row.get("dtime") or row.get("last_update") or "",
                    "creator_nama": row.get("creator_nama") or "",
                    "opname_jenis": row.get("opname_jenis") or "",
                }
                if "gudang_id" in insert_columns:
                    raw_gudang_id = _pick_value(
                        row,
                        optional_column_candidates["gudang_id"],
                        0,
                    )
                    try:
                        gudang_id_int = int(float(raw_gudang_id or 0))
                    except _NUMERIC_PARSE_EXCEPTIONS:
                        gudang_id_int = 0
                    if gudang_id_int == 0:
                        gudang_id_int = _fallback_gudang_id_from_branch_id(row.get("id"))
                    values_by_column["gudang_id"] = gudang_id_int
                if "gudang_nama" in insert_columns:
                    values_by_column["gudang_nama"] = _pick_value(
                        row,
                        optional_column_candidates["gudang_nama"],
                        "",
                    )
                cur.execute(
                    insert_sql,
                    tuple(values_by_column.get(col) for col in insert_columns),
                )
            conn.commit()
        finally:
            conn.close()

    def search_store_branches(self, web_admin_url, toko_id):
        trace_id = self._new_trace_id("search_store_branches")
        web_admin_val = str(web_admin_url or "").strip()
        if not web_admin_val:
            return self._build_error_result(
                level="warning",
                message="Web Admin wajib diisi.",
                error_code="AUTH_WEB_ADMIN_REQUIRED",
                reason="missing_web_admin_url",
                trace_id=trace_id,
            )

        try:
            toko_id_int = self._parse_toko_id(toko_id)
        except ValueError as e:
            return self._build_error_result(
                level="warning",
                message=str(e),
                error_code="AUTH_TOKO_ID_INVALID",
                reason="invalid_toko_id",
                trace_id=trace_id,
            )

        try:
            self.set_web_admin_url(web_admin_val)
        except _CONFIG_WRITE_EXCEPTIONS as e:
            safe_error, _ = sanitize_ui_message("warning", str(e))
            return self._build_error_result(
                level="warning",
                message=f"Web Admin tidak valid.\n{safe_error}",
                error_code="AUTH_WEB_ADMIN_INVALID",
                reason="invalid_web_admin_url",
                trace_id=trace_id,
            )

        try:
            save_config({"toko_id": toko_id_int})
        except _CONFIG_WRITE_EXCEPTIONS:
            pass

        # edited by glg
        # Saat "Cari Toko", cek dulu apakah machine ini sudah terdaftar di server.
        # Jika sudah terdaftar, tidak perlu proses registrasi ulang.
        machine_id = self.get_machine_id()
        existing_probe = self._probe_existing_device_on_server(
            machine_id=machine_id,
            toko_id_int=toko_id_int,
        )
        if existing_probe.get("status") == 200 and existing_probe.get("ok"):
            data_reg = existing_probe.get("data_reg") or {}
            cabang_server = str(data_reg.get("cabang_nama") or "").strip()
            return self._get_response_mapper().build_existing_registered_result(
                cabang_server=cabang_server,
                auto_close=True,
            )
        if existing_probe.get("status") == 202:
            return self._get_response_mapper().build_pending_approval_result(auto_close=True)
        existing_result = existing_probe.get("result")
        if isinstance(existing_result, dict):
            if existing_probe.get("status") not in (None, 404) and not is_device_not_registered(existing_result):
                raw_reason = existing_result.get("reason") or existing_result.get("message") or ""
                LOGGER.warning(
                    "Cek existing device saat cari toko tidak valid, lanjut load cabang. status=%s reason=%s",
                    existing_probe.get("status"),
                    raw_reason,
                )

        rows = get_branch_rows(toko_id=toko_id_int) or []
        filtered_rows = []
        for row in rows:
            try:
                row_toko = int(row.get("toko_id"))
            except _NUMERIC_PARSE_EXCEPTIONS:
                continue
            if row_toko != toko_id_int:
                continue
            if int(0 if row.get("trash") in (None, "") else row.get("trash")) != 0:
                continue
            # edited by glg
            # Hindari fallback "or 1" karena status=0 (nonaktif) harus tetap nonaktif.
            if int(1 if row.get("status") in (None, "") else row.get("status")) != 1:
                continue
            filtered_rows.append(row)

        if not filtered_rows:
            try:
                self._replace_branches_for_toko(toko_id_int, [])
            except _DB_OPERATION_EXCEPTIONS:
                pass
            return self._build_error_result(
                level="warning",
                message=f"Cabang untuk Toko ID {toko_id_int} tidak ditemukan.",
                error_code="AUTH_BRANCHES_NOT_FOUND",
                reason="branches_not_found",
                trace_id=trace_id,
            )

        branches = self._build_branch_options_from_rows(filtered_rows)
        if not branches:
            return self._build_error_result(
                level="warning",
                message=f"Cabang untuk Toko ID {toko_id_int} tidak tersedia.",
                error_code="AUTH_BRANCHES_EMPTY",
                reason="branches_empty",
                trace_id=trace_id,
            )

        # Cache ke lokal tetap dicoba, tapi tidak boleh memblokir dropdown dinamis dari endpoint.
        try:
            self._replace_branches_for_toko(toko_id_int, filtered_rows)
        except _DB_OPERATION_EXCEPTIONS as e:
            LOGGER.warning("Gagal menyimpan daftar cabang lokal (non-blocking): %s", e)

        return {
            "ok": True,
            "level": "information",
            "message": f"Ditemukan {len(branches)} cabang untuk Toko ID {toko_id_int}.",
            "branches": branches,
        }

    def get_web_admin_url(self):
        endpoint_cfg = read_endpoint_config() or {}
        return str(endpoint_cfg.get("api_base_url") or "").strip()

    def get_toko_id_default(self):
        cfg = read_config() or {}
        raw_value = cfg.get("toko_id")
        if raw_value is None:
            return ""
        return str(raw_value).strip()

    def set_web_admin_url(self, web_admin_url):
        raw = str(web_admin_url or "").strip()
        if not raw:
            raise ValueError("Web Admin wajib diisi.")
        normalized = normalize_base_url(raw)
        save_endpoint_config({"api_base_url": normalized})
        return normalized

    def get_device_summary(self):
        try:
            return f"{socket.gethostname()} - {platform.system()} {platform.release()}"
        except OSError:
            return "Informasi device tidak tersedia"

    def get_machine_id(self):
        try:
            device = get_device_id()
            if device:
                return str(device)
        except _DEVICE_RESOLVE_EXCEPTIONS:
            pass

        try:
            node = uuid.getnode()
            if (node >> 40) % 2 == 0:
                return str(node)
        except _DEVICE_RESOLVE_EXCEPTIONS:
            pass

        return str(uuid.uuid4())

    # edited by glg
    def _extract_status_code(self, result):
        return self._get_response_mapper().extract_status_code(result)

    def _extract_data_reg(self, result):
        return self._get_response_mapper().extract_data_reg(result)

    def _probe_existing_device_on_server(self, machine_id, toko_id_int):
        retry_policy = self._get_retry_policy()
        cfg = read_config() or {}
        max_attempts = retry_policy.resolve_probe_attempt_limit(cfg)
        result = None
        status_code = None
        for attempt in range(1, max_attempts + 1):
            try:
                result = cek_device_ke_server(machine_id)
            except _DEVICE_RESOLVE_EXCEPTIONS:
                if retry_policy.should_retry_probe_exception(attempt, max_attempts):
                    continue
                return {
                    "status": None,
                    "ok": False,
                    "result": None,
                    "error_code": "AUTH_PROBE_EXISTING_DEVICE_FAILED",
                    "reason": "probe_existing_device_failed",
                    "trace_id": self._new_trace_id("probe_existing_device"),
                }
            status_code = self._extract_status_code(result)
            if retry_policy.should_retry_probe_status(attempt, max_attempts, status_code):
                continue
            break

        if status_code == 200:
            data_reg = self._extract_data_reg(result)
            if not data_reg:
                data_reg = {}
            data_reg.setdefault("machine_id", machine_id)
            data_reg.setdefault("status", 1)
            if toko_id_int and not data_reg.get("toko_id"):
                data_reg["toko_id"] = toko_id_int

            saved = simpan_device_lokal(data_reg, device_id=machine_id)
            return {
                "status": 200,
                "ok": bool(saved),
                "result": result,
                "data_reg": data_reg,
            }

        if status_code == 202:
            data_reg = self._extract_data_reg(result)
            if not data_reg:
                data_reg = {}
            data_reg.setdefault("machine_id", machine_id)
            data_reg["status"] = 0
            if toko_id_int and not data_reg.get("toko_id"):
                data_reg["toko_id"] = toko_id_int
            simpan_device_pending_lokal(data_reg, device_id=machine_id)
            return {
                "status": 202,
                "ok": True,
                "result": result,
                "data_reg": data_reg,
            }

        return {
            "status": status_code,
            "ok": False,
            "result": result,
            "data_reg": {},
            "error_code": "AUTH_PROBE_EXISTING_DEVICE_NOT_REGISTERED",
            "reason": "device_not_registered",
            "trace_id": self._new_trace_id("probe_existing_device"),
        }

    def register_device(self, alias, keterangan, cabang_nama, cabang_id, nama_pengguna, web_admin_url, toko_id):
        trace_id = self._new_trace_id("register_device")
        request_builder = self._get_request_builder()
        response_mapper = self._get_response_mapper()
        inputs = request_builder.normalize_register_inputs(
            alias=alias,
            keterangan=keterangan,
            cabang_nama=cabang_nama,
            cabang_id=cabang_id,
            nama_pengguna=nama_pengguna,
            web_admin_url=web_admin_url,
            toko_id=toko_id,
        )
        alias_val = inputs.get("alias") or ""
        ket_val = inputs.get("keterangan") or ""
        cabang_name_val = inputs.get("cabang_nama") or ""
        cabang_id_val = inputs.get("cabang_id") or ""
        nama_val = inputs.get("nama_pengguna") or ""
        web_admin_val = inputs.get("web_admin_url") or ""
        toko_id_val = inputs.get("toko_id") or ""

        try:
            toko_id_int = self._parse_toko_id(toko_id_val)
        except ValueError:
            return self._build_error_result(
                level="warning",
                message="Toko ID wajib angka lebih dari 0.",
                error_code="AUTH_TOKO_ID_INVALID",
                reason="invalid_toko_id",
                trace_id=trace_id,
            )

        try:
            self.set_web_admin_url(web_admin_val)
        except _CONFIG_WRITE_EXCEPTIONS as e:
            safe_error, _ = sanitize_ui_message("warning", str(e))
            return self._build_error_result(
                level="warning",
                message=f"Web Admin tidak valid.\n{safe_error}",
                error_code="AUTH_WEB_ADMIN_INVALID",
                reason="invalid_web_admin_url",
                trace_id=trace_id,
            )

        try:
            save_config({"toko_id": toko_id_int})
        except _CONFIG_WRITE_EXCEPTIONS:
            pass

        machine_id = self.get_machine_id()

        # edited by glg
        # Jika device sudah terdaftar di server, jangan kirim registrasi ulang.
        # Simpan lokal lalu lanjut login.
        existing_probe = self._probe_existing_device_on_server(machine_id=machine_id, toko_id_int=toko_id_int)
        if existing_probe.get("status") == 200:
            if not existing_probe.get("ok"):
                return self._build_error_result(
                    level="critical",
                    message="Device sudah terdaftar di server, tetapi gagal menyimpan data lokal.",
                    error_code="AUTH_DEVICE_LOCAL_PERSIST_FAILED",
                    reason="device_local_persist_failed",
                    trace_id=trace_id,
                )
            data_reg = existing_probe.get("data_reg") or {}
            cabang_server = str(data_reg.get("cabang_nama") or "").strip()
            return response_mapper.build_existing_registered_result(cabang_server=cabang_server)

        if existing_probe.get("status") == 202:
            return response_mapper.build_pending_approval_result()

        if not request_builder.has_required_register_fields(inputs):
            return self._build_error_result(
                level="warning",
                message="Harap lengkapi field bertanda *",
                error_code="AUTH_REGISTRATION_FIELDS_REQUIRED",
                reason="missing_required_fields",
                trace_id=trace_id,
            )

        browser_verif = request_builder.generate_browser_verif()
        payload = request_builder.build_registration_payload(
            machine_id=machine_id,
            browser_verif=browser_verif,
            alias=alias_val,
            keterangan=ket_val,
            cabang_nama=cabang_name_val,
            cabang_id=cabang_id_val,
            nama_pengguna=nama_val,
            toko_id_int=toko_id_int,
        )

        try:
            result = post_device_registration(payload)
            status = result.get("status") if isinstance(result, dict) else result
            ok = response_mapper.is_registration_success_status(status)

            if ok:
                pending_payload = request_builder.build_pending_local_payload(
                    machine_id=machine_id,
                    browser_verif=browser_verif,
                    alias=alias_val,
                    keterangan=ket_val,
                    cabang_nama=cabang_name_val,
                    cabang_id=cabang_id_val,
                    nama_pengguna=nama_val,
                    toko_id_int=toko_id_int,
                    cpu_info=payload.get("cpu_info", ""),
                    com_info=payload.get("com_info", ""),
                )
                simpan_device_pending_lokal(
                    pending_payload,
                    device_id=machine_id,
                )
                set_bool("first_install_done", True)
                set_bool("first_sync_pending", True)
                set_bool("per_employee_seed_pending", True)
                set_bool("per_employee_seed_done", False)
                return {
                    "ok": True,
                    "level": "information",
                    "message": "Registrasi berhasil dikirim. Mohon tunggu persetujuan pusat.",
                }

            reason = response_mapper.extract_failure_reason(result)

            # edited by glg
            # Fallback: server menolak registrasi baru karena cabang konflik,
            # cek ulang status existing device untuk sinkronisasi lokal.
            if response_mapper.is_conflict_cabang_reason(reason):
                conflict_probe = self._probe_existing_device_on_server(machine_id=machine_id, toko_id_int=toko_id_int)
                if conflict_probe.get("status") == 200 and conflict_probe.get("ok"):
                    data_reg = conflict_probe.get("data_reg") or {}
                    cabang_server = str(data_reg.get("cabang_nama") or "").strip()
                    out = response_mapper.build_existing_registered_result(cabang_server=cabang_server)
                    out["message"] = (
                        "Device sudah terdaftar di server. "
                        "Registrasi ulang tidak diperlukan, aplikasi akan lanjut ke login."
                    )
                    if cabang_server:
                        out["message"] = (
                            f"Device sudah terdaftar pada cabang '{cabang_server}'. "
                            "Registrasi ulang tidak diperlukan, aplikasi akan lanjut ke login."
                        )
                    return out
                if conflict_probe.get("status") == 202:
                    return response_mapper.build_pending_approval_result()

            safe_reason, _ = sanitize_ui_message("critical", reason)
            return self._build_error_result(
                level="critical",
                message=f"Registrasi gagal: {safe_reason}",
                error_code="AUTH_REGISTRATION_REJECTED",
                reason="registration_rejected",
                trace_id=trace_id,
            )
        except (RuntimeError, ValueError, TypeError, OSError, ConnectionError) as e:
            pending_payload = request_builder.build_pending_local_payload(
                machine_id=machine_id,
                browser_verif=browser_verif,
                alias=alias_val,
                keterangan=ket_val,
                cabang_nama=cabang_name_val,
                cabang_id=cabang_id_val,
                nama_pengguna=nama_val,
                toko_id_int=toko_id_int,
            )
            simpan_device_pending_lokal(
                pending_payload,
                device_id=machine_id,
            )
            safe_error, _ = sanitize_ui_message("critical", str(e))
            return self._build_error_result(
                level="critical",
                message=f"Gagal mengirim registrasi.\n{safe_error}",
                error_code="AUTH_REGISTRATION_SEND_FAILED",
                reason="registration_send_failed",
                trace_id=trace_id,
            )
