import json
import os
import sqlite3
import hashlib
import csv
import base64
import re
import time
import threading
from datetime import datetime
from urllib.parse import quote
from pypos.core.base_model import BaseModel
from pypos.core.utils.db_helper import get_company_profile as fetch_company_profile
from pypos.core.utils.db_helper import connect_sqlite
from pypos.core.utils.path_utils import get_app_data_resource_dir, get_db_path
from pypos.core.utils.sql_query_builder import render_sql_template
from pypos.core.utils.config_utils import (
    read_config,
    read_app_settings,
    read_endpoint_config,
    normalize_base_url,
)
from pypos.core.utils.http_retry import get_with_retry
from pypos.modules.printer.config.printer_config import DEFAULT_PAPER_SIZE
from pypos.modules.printer.config.print_profile_config import SETTLEMENT_PRINT_OPTION_KEYS

class PrinterSettingsModel(BaseModel):
    """Model untuk menyimpan konfigurasi printer pada berkas JSON."""
    def __init__(self):
        super().__init__()
        self.file_path = os.path.join(get_app_data_resource_dir(), 'printers.json')
        self.file_path = os.path.abspath(self.file_path)
        self.file_backup_path = self.file_path + ".bak"
        # edited by glg
        # Serialisasi akses tulis baca file konfigurasi printer agar tidak race
        # saat dipakai dari beberapa event UI.
        self._storage_lock = threading.RLock()
        # edited by glg
        # Hindari retry URL logo berulang saat host sedang down agar hot-path print tidak blocking.
        self._logo_resolve_fail_until = {}
        self._logo_resolve_fail_ttl_sec = 60
        if not os.path.exists(self.file_path):
            self.save_printers([])
        elif not os.path.exists(self.file_backup_path):
            try:
                payload = self._read_printers_payload(self.file_path)
                self._atomic_write_json(self.file_backup_path, payload)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass

    # edited by glg
    def _build_tmp_path(self, target_path: str) -> str:
        stamp = int(time.time() * 1000)
        return f"{target_path}.tmp.{os.getpid()}.{stamp}"

    # edited by glg
    def _atomic_write_json(self, target_path: str, payload: dict) -> None:
        parent_dir = os.path.dirname(target_path) or "."
        os.makedirs(parent_dir, exist_ok=True)
        tmp_path = self._build_tmp_path(target_path)
        try:
            with open(tmp_path, "w", encoding="utf-8") as fh:
                json.dump(payload, fh, indent=2)
                fh.flush()
                os.fsync(fh.fileno())
            os.replace(tmp_path, target_path)
        finally:
            if os.path.exists(tmp_path):
                try:
                    os.remove(tmp_path)
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    pass

    # edited by glg
    def _atomic_write_csv_rows(self, target_path: str, rows) -> None:
        parent_dir = os.path.dirname(target_path) or "."
        os.makedirs(parent_dir, exist_ok=True)
        tmp_path = self._build_tmp_path(target_path)
        try:
            with open(tmp_path, "w", encoding="utf-8", newline="") as fh:
                writer = csv.writer(fh)
                writer.writerows(rows)
                fh.flush()
                os.fsync(fh.fileno())
            os.replace(tmp_path, target_path)
        finally:
            if os.path.exists(tmp_path):
                try:
                    os.remove(tmp_path)
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    pass

    # edited by glg
    def _read_printers_payload(self, target_path: str) -> dict:
        with open(target_path, "r", encoding="utf-8") as fh:
            data = json.load(fh)
        if not isinstance(data, dict):
            return {"printers": []}
        printers = data.get("printers")
        if not isinstance(printers, list):
            printers = []
        return {"printers": printers}

    # edited by glg
    def _recover_printers_payload(self) -> dict:
        try:
            backup_payload = self._read_printers_payload(self.file_backup_path)
            self._atomic_write_json(self.file_path, backup_payload)
            return backup_payload
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
            self.log_warning(f"Gagal recovery printers.json dari backup: {exc}")
        empty_payload = {"printers": []}
        self._atomic_write_json(self.file_path, empty_payload)
        self._atomic_write_json(self.file_backup_path, empty_payload)
        return empty_payload

    def build_receipt_qr_data(self, transaksi_data_dict: dict = None, wifi_code: str = "") -> str:
        tx = transaksi_data_dict or {}
        explicit_qr = str(tx.get("qr_data") or "").strip()
        if explicit_qr:
            return explicit_qr

        nomor = str(tx.get("nomer") or tx.get("invoice") or "").strip()
        wifi_val = str(wifi_code or tx.get("wifi_code") or "").strip()
        cfg = read_endpoint_config()
        base_url = normalize_base_url(str(cfg.get("api_base_url") or ""))
        qr_tpl = str(cfg.get("ep_receipt_qr") or "").strip()

        if qr_tpl and base_url:
            try:
                if any(k in qr_tpl for k in ("{nomer}", "{invoice}", "{wifi_code}")):
                    built = qr_tpl.format(nomer=nomor, invoice=nomor, wifi_code=wifi_val)
                else:
                    separator = "&" if "?" in qr_tpl else "?"
                    built = f"{qr_tpl}{separator}nomer={quote(nomor)}"
                    if wifi_val:
                        built = f"{built}&wifi_code={quote(wifi_val)}"
                if built.startswith(("http://", "https://")):
                    return built
                if not built.startswith("/"):
                    built = "/" + built
                return f"{base_url}{built}"
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
                self.log_warning(f"Gagal build ep_receipt_qr: {exc}")

        if nomor:
            return nomor
        if wifi_val:
            return f"WIFI:{wifi_val}"
        return ""

    def get_default_printer_paper_size(self) -> str:
        cfg = read_app_settings()
        return str(cfg.get("printer_default_paper_size") or DEFAULT_PAPER_SIZE)

    def get_demo_print_context(self, qr_builder=None) -> dict:
        cfg = read_app_settings()
        demo_items = cfg.get("print_demo_items") if isinstance(cfg.get("print_demo_items"), list) else []
        items = []
        for row in demo_items:
            if not isinstance(row, dict):
                continue
            items.append({
                "name": str(row.get("name") or "").strip(),
                "qty": int(row.get("qty") or 0),
                "price": int(float(row.get("price") or 0)),
                "disc": int(float(row.get("disc") or 0)),
                "free": bool(int(row.get("free") or 0)),
            })

        payment = {
            "method": str(cfg.get("print_demo_payment_method") or "").strip(),
            "card_brand": str(cfg.get("print_demo_card_brand") or "").strip(),
            "last4": str(cfg.get("print_demo_card_last4") or "").strip(),
            "approval_code": str(cfg.get("print_demo_approval_code") or "").strip(),
        }

        transaksi_data_dict = {
            "nomer": str(cfg.get("print_demo_nomor") or "").strip(),
            "dtime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "oleh_nama": str(cfg.get("print_demo_kasir_nama") or "").strip(),
            "customers_nama": str(cfg.get("print_demo_customer_nama") or "").strip(),
            "transaksi_bulat": int(float(cfg.get("print_demo_transaksi_bulat") or 0)),
            "ppn_persen": int(float(cfg.get("print_demo_ppn_persen") or 0)),
            "diskon_persen": int(float(cfg.get("print_demo_diskon_persen") or 0)),
            "transaksi_nilai": int(float(cfg.get("print_demo_transaksi_nilai") or 0)),
        }

        wifi_code = str(cfg.get("print_demo_wifi_code") or "").strip()
        qr_data = ""
        if callable(qr_builder):
            try:
                qr_data = str(qr_builder(transaksi_data_dict=transaksi_data_dict, wifi_code=wifi_code) or "").strip()
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                qr_data = ""
        if not qr_data:
            qr_data = str(transaksi_data_dict.get("nomer") or wifi_code or "").strip()

        return {
            "items": items,
            "payment": payment,
            "wifi_code": wifi_code,
            "qr_data": qr_data,
            "transaksi_data_dict": transaksi_data_dict,
        }

    def _get_setting_struk_csv_path(self) -> str:
        return os.path.abspath(os.path.join(get_app_data_resource_dir(), "setting_struk.csv"))

    def get_print_mode(self) -> str:
        csv_path = self._get_setting_struk_csv_path()
        mode = "preview"
        if os.path.exists(csv_path):
            try:
                with open(csv_path, "r", encoding="utf-8", newline="") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) >= 2 and row[0].strip() == "print_mode":
                            return (row[1].strip().lower() or mode)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass
            try:
                with open(csv_path, "r", encoding="utf-8") as f:
                    for line in f:
                        if "mode=" in line:
                            return line.strip().split("=")[-1].lower()
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass
        return mode

    def set_print_mode(self, mode: str) -> None:
        csv_path = self._get_setting_struk_csv_path()
        mode_value = (mode or "preview").strip().lower()
        rows = []
        updated = False
        if os.path.exists(csv_path):
            try:
                with open(csv_path, "r", encoding="utf-8", newline="") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) >= 1 and row[0].strip() == "print_mode":
                            row = ["print_mode", mode_value]
                            updated = True
                        rows.append(row)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
                self.log_warning(f"Gagal baca setting_struk.csv saat set_print_mode: {exc}")
        if not updated:
            rows.append(["print_mode", mode_value])
        with self._storage_lock:
            self._atomic_write_csv_rows(csv_path, rows)

    def get_history_retention_days(self) -> int:
        csv_path = self._get_setting_struk_csv_path()
        if os.path.exists(csv_path):
            try:
                with open(csv_path, "r", encoding="utf-8", newline="") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) >= 2 and row[0].strip() == "history_retention_days":
                            return int(row[1].strip())
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                return 30
        return 30

    def set_history_retention_days(self, days: int) -> None:
        csv_path = self._get_setting_struk_csv_path()
        rows = []
        updated = False
        value = str(int(days))
        if os.path.exists(csv_path):
            try:
                with open(csv_path, "r", encoding="utf-8", newline="") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) >= 1 and row[0].strip() == "history_retention_days":
                            row = ["history_retention_days", value]
                            updated = True
                        rows.append(row)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
                self.log_warning(f"Gagal baca setting_struk.csv saat set_history_retention_days: {exc}")
        if not updated:
            rows.append(["history_retention_days", value])
        with self._storage_lock:
            self._atomic_write_csv_rows(csv_path, rows)

    # edited by glg
    def _get_simple_csv_value(self, key: str, default=""):
        csv_path = self._get_setting_struk_csv_path()
        key_name = str(key or "").strip()
        if not key_name or not os.path.exists(csv_path):
            return default
        try:
            with open(csv_path, "r", encoding="utf-8", newline="") as f:
                reader = csv.reader(f)
                for row in reader:
                    if len(row) >= 2 and str(row[0] or "").strip() == key_name:
                        return str(row[1] or "").strip()
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return default
        return default

    # edited by glg
    def _set_simple_csv_value(self, key: str, value) -> None:
        csv_path = self._get_setting_struk_csv_path()
        key_name = str(key or "").strip()
        if not key_name:
            return

        rows = []
        updated = False
        value_str = str(value if value is not None else "").strip()
        if os.path.exists(csv_path):
            try:
                with open(csv_path, "r", encoding="utf-8", newline="") as f:
                    reader = csv.reader(f)
                    for row in reader:
                        if len(row) >= 1 and str(row[0] or "").strip() == key_name:
                            row = [key_name, value_str]
                            updated = True
                        rows.append(row)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
                self.log_warning(f"Gagal baca setting_struk.csv saat update key '{key_name}': {exc}")
        if not updated:
            rows.append([key_name, value_str])
        with self._storage_lock:
            self._atomic_write_csv_rows(csv_path, rows)

    # edited by glg
    def _to_bool_text(self, value, default: bool = False) -> str:
        if value is None:
            return "1" if bool(default) else "0"
        normalized = str(value).strip().lower()
        if normalized in {"1", "true", "yes", "on"}:
            return "1"
        if normalized in {"0", "false", "no", "off"}:
            return "0"
        try:
            return "1" if int(float(value)) > 0 else "0"
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return "1" if bool(default) else "0"

    # edited by glg
    def _parse_bool_text(self, value, default: bool = False) -> bool:
        if value is None:
            return bool(default)
        normalized = str(value).strip().lower()
        if normalized in {"1", "true", "yes", "on"}:
            return True
        if normalized in {"0", "false", "no", "off"}:
            return False
        try:
            return int(float(value)) > 0
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return bool(default)

    # edited by glg
    def get_settlement_print_option(self, option_key: str, default: bool = False) -> bool:
        key_map = SETTLEMENT_PRINT_OPTION_KEYS if isinstance(SETTLEMENT_PRINT_OPTION_KEYS, dict) else {}
        storage_key = str(key_map.get(option_key) or "").strip()
        if not storage_key:
            return bool(default)
        raw_value = self._get_simple_csv_value(storage_key, "")
        if str(raw_value).strip() == "":
            return bool(default)
        return self._parse_bool_text(raw_value, default=default)

    # edited by glg
    def set_settlement_print_option(self, option_key: str, enabled: bool) -> None:
        key_map = SETTLEMENT_PRINT_OPTION_KEYS if isinstance(SETTLEMENT_PRINT_OPTION_KEYS, dict) else {}
        storage_key = str(key_map.get(option_key) or "").strip()
        if not storage_key:
            return
        self._set_simple_csv_value(storage_key, self._to_bool_text(enabled, default=False))

    def load_setting_struk_csv(self, cabang_id=None) -> dict:
        csv_path = self._get_setting_struk_csv_path()
        if not os.path.exists(csv_path):
            return {}
        try:
            with open(csv_path, "r", encoding="utf-8", newline="") as f:
                reader = csv.DictReader(f)
                first_valid = None
                for row in reader:
                    row_id = (row.get("id") or "").strip()
                    if not row_id.isdigit():
                        continue
                    if not first_valid:
                        first_valid = row
                    if cabang_id is None:
                        continue
                    if str(row.get("cabang_id") or "").strip() == str(cabang_id):
                        return self._map_setting_row(row)
                if first_valid:
                    return self._map_setting_row(first_valid)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return {}
        return {}

    def _map_setting_row(self, row: dict) -> dict:
        return {
            "header1": (row.get("header1") or "").strip(),
            "header2": (row.get("header2") or "").strip(),
            "header3": (row.get("header3") or "").strip(),
            "footer1": (row.get("footer1") or "").strip(),
            "footer2": (row.get("footer2") or "").strip(),
            "footer3": (row.get("footer3") or "").strip(),
            "logo": (row.get("logo") or "").strip(),
        }

    def get_company_profile(self):
        return fetch_company_profile(get_db_path())

    def get_receipt_setting(self, cabang_id=None) -> dict:
        profile = self.get_company_profile()
        company_setting = self.build_setting_from_company_profile(profile)
        source_setting = self.get_setting_struk(cabang_id) or self.load_setting_struk_csv(cabang_id)
        merged = {}
        for key in ("header1", "header2", "header3", "footer1", "footer2", "footer3", "logo"):
            val = company_setting.get(key, "")
            if not val:
                val = source_setting.get(key, "")
            merged[key] = val
        return merged

    def get_setting_struk(self, cabang_id=None) -> dict:
        try:
            db_path = get_db_path()
            conn = connect_sqlite(db_path)
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()
            cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='setting_struk'"
            )
            if not cur.fetchone():
                conn.close()
                return {}
            cur.execute("PRAGMA table_info(setting_struk)")
            cols = [row[1] for row in cur.fetchall()]
            def pick_col(candidates):
                for c in candidates:
                    if c in cols:
                        return c
                return None
            col_cabang = pick_col(["cabang_id", "cabangID", "cabang"])
            if cabang_id is None:
                cabang_id = None
            if col_cabang and cabang_id is not None:
                query = render_sql_template(
                    "SELECT * FROM setting_struk WHERE {col_cabang} = ? ORDER BY id DESC LIMIT 1",
                    col_cabang=col_cabang,
                )
                cur.execute(
                    query,
                    (cabang_id,),
                )
            else:
                cur.execute("SELECT * FROM setting_struk ORDER BY id DESC LIMIT 1")
            row = cur.fetchone()
            conn.close()
            if not row:
                return {}
            row_dict = dict(row)
            def get_val(keys):
                for k in keys:
                    if k in row_dict and row_dict.get(k) is not None:
                        # edited by glg
                        # Preserve angka 0 (contoh cetak_logo=0), jangan dikosongkan oleh operator `or`.
                        return str(row_dict.get(k)).strip()
                return ""
            cetak_logo_val = get_val(["cetak_logo"])
            setting = {
                "header1": get_val(["header1", "header_1", "header_satu"]),
                "header2": get_val(["header2", "header_2", "header_dua"]),
                "header3": get_val(["header3", "header_3", "header_tiga"]),
                "footer1": get_val(["footer1", "footer_1", "footer_satu"]),
                "footer2": get_val(["footer2", "footer_2", "footer_dua"]),
                "footer3": get_val(["footer3", "footer_3", "footer_tiga"]),
                "logo": get_val(["logo", "logo_path", "logo_img", "file_name"]),
                "cetak_logo": cetak_logo_val,
            }
            # edited by glg
            cetak_logo_norm = str(cetak_logo_val).strip().lower()
            if cetak_logo_norm in {"0", "false", "no"}:
                setting["logo"] = ""
            else:
                setting["logo"] = self._resolve_setting_struk_logo(setting.get("logo"), cabang_id)
            return setting
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return {}

    def _resolve_setting_struk_logo(self, logo_value: str, cabang_id=None) -> str:
        logo_value = (logo_value or "").strip()
        if not logo_value:
            return ""

        logo_dir = os.path.join(get_app_data_resource_dir(), "logo")
        os.makedirs(logo_dir, exist_ok=True)

        def _delete_old_cache(prefix: str, exclude_name: str = ""):
            try:
                for name in os.listdir(logo_dir):
                    if name.startswith(prefix):
                        if exclude_name and name == exclude_name:
                            continue
                        try:
                            os.remove(os.path.join(logo_dir, name))
                        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                            pass
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass

        cfg = read_config()
        base_url = (cfg.get("api_base_url") or "").rstrip("/")
        timeout = int(cfg.get("request_timeout") or 3)

        def _normalize_url(value: str) -> str:
            val = value.strip()
            if val.lower().startswith(("http://", "https://")):
                return val
            if val.startswith("/"):
                return f"{base_url}{val}" if base_url else ""
            if "/" in val:
                return f"{base_url}/{val}" if base_url else ""
            # asumsi nama file di public/logo
            if base_url:
                return f"{base_url}/public/logo/{val}"
            return ""

        local_path = os.path.join(logo_dir, logo_value)
        has_local_file = os.path.exists(local_path)

        url = _normalize_url(logo_value)
        if not url:
            if has_local_file:
                return logo_value
            return ""

        ext = os.path.splitext(url.split("?")[0])[1].lower()
        if ext not in (".png", ".jpg", ".jpeg", ".bmp", ".gif"):
            ext = ".png"
        cache_prefix = f"setting_struk_logo_{cabang_id or 'all'}_"
        # edited by glg
        # Cache key bukan kredensial, namun tetap gunakan SHA-256 agar konsisten policy hardening.
        cache_name = f"{cache_prefix}{hashlib.sha256(url.encode('utf-8')).hexdigest()}{ext}"
        cache_path = os.path.join(logo_dir, cache_name)
        # edited by glg
        # Jika cache sudah ada, gunakan langsung tanpa hit endpoint ulang.
        if os.path.exists(cache_path):
            return cache_name

        now_ts = time.time()
        fail_key = f"{cabang_id or 'all'}:{url}"
        fail_until = float(self._logo_resolve_fail_until.get(fail_key) or 0)
        if fail_until > now_ts:
            if has_local_file:
                return logo_value
            return ""

        tmp_path = cache_path + ".tmp"
        try:
            resp = get_with_retry(url, timeout=timeout)

            if resp is not None and resp.content:
                with open(tmp_path, "wb") as f:
                    f.write(resp.content)
                if has_local_file and os.path.abspath(local_path) != os.path.abspath(cache_path):
                    try:
                        os.remove(local_path)
                    except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                        pass
                os.replace(tmp_path, cache_path)
                _delete_old_cache(cache_prefix, exclude_name=cache_name)
                self._logo_resolve_fail_until.pop(fail_key, None)
                return cache_name
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            self._logo_resolve_fail_until[fail_key] = now_ts + max(float(timeout or 1), float(self._logo_resolve_fail_ttl_sec))
            try:
                if os.path.exists(tmp_path):
                    os.remove(tmp_path)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass
            # jika cache lama masih ada, gunakan cache lama
            if os.path.exists(cache_path):
                return cache_name
            # fallback ke file lokal lama jika ada
            if has_local_file:
                return logo_value
            return ""
        return ""

    def build_setting_from_company_profile(self, profile: dict) -> dict:
        if not isinstance(profile, dict):
            return {}
        setting = {}
        name = (profile.get("alias") or profile.get("nama") or "").strip()
        if name:
            setting["header1"] = name
        address = (
            (profile.get("alamat_1") or "").strip()
            or (profile.get("address") or "").strip()
            or (profile.get("alamat") or "").strip()
        )
        if not address:
            parts = [
                profile.get("alamat"),
                profile.get("kelurahan"),
                profile.get("kecamatan"),
                profile.get("kabupaten"),
                profile.get("propinsi"),
                profile.get("kodepos"),
            ]
            address = ", ".join([str(p).strip() for p in parts if p])
        if address:
            address_lines = [l.strip() for l in re.split(r"\r?\n", address) if l.strip()]
            setting["header2"] = address_lines[0]
            if len(address_lines) > 1:
                setting["header3"] = address_lines[1]
        phones = [profile.get("tlp"), profile.get("tlp_2"), profile.get("tlp_3")]
        phones = [str(p).strip() for p in phones if p]
        if phones:
            telp_text = f"Telp: {' / '.join(phones)}"
            if setting.get("header3"):
                setting["header3"] = f"{setting['header3']} | {telp_text}"
            else:
                setting["header3"] = telp_text
        extra = (profile.get("extra_struk") or "").strip()
        if extra:
            lines = [l.strip() for l in re.split(r"\r?\n", extra) if l.strip()]
            if len(lines) > 0:
                setting["footer1"] = lines[0]
            if len(lines) > 1:
                setting["footer2"] = lines[1]
            if len(lines) > 2:
                setting["footer3"] = lines[2]
        logo_path = self.resolve_company_logo_path(profile)
        if logo_path:
            setting["logo"] = logo_path
        return setting

    def resolve_company_logo_path(self, profile: dict) -> str:
        if not isinstance(profile, dict):
            return ""
        logo_img = profile.get("logo_img")
        if logo_img:
            try:
                logo_str = str(logo_img)
                if "base64," in logo_str:
                    logo_str = logo_str.split("base64,", 1)[1]
                raw = base64.b64decode(logo_str)
                cache_path = self._get_company_logo_cache_path()
                if cache_path:
                    with open(cache_path, "wb") as f:
                        f.write(raw)
                    return cache_path
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass
        file_name = (profile.get("file_name") or "").strip()
        file_ext = (profile.get("file_ext") or "").lower().strip()
        if file_name and (
            file_ext in (".png", ".jpg", ".jpeg", ".bmp", ".gif")
            or file_name.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif"))
        ):
            full_logo_path = os.path.join(get_app_data_resource_dir(), "logo", file_name)
            if os.path.exists(full_logo_path):
                return full_logo_path
        return ""

    def _get_company_logo_cache_path(self):
        logo_dir = os.path.join(get_app_data_resource_dir(), "logo")
        try:
            os.makedirs(logo_dir, exist_ok=True)
        except OSError:
            return None
        return os.path.join(logo_dir, "company_logo.png")

    def refresh_company_logo_assets(self, force_refresh=True) -> bool:
        try:
            profile = self.get_company_profile()
            target_path = self._get_company_logo_cache_path()
            if not target_path:
                return False
            if force_refresh and os.path.exists(target_path):
                try:
                    os.remove(target_path)
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    pass
            logo_img = profile.get("logo_img") if isinstance(profile, dict) else None
            if not logo_img:
                return True
            try:
                logo_str = str(logo_img)
                if "base64," in logo_str:
                    logo_str = logo_str.split("base64,", 1)[1]
                raw = base64.b64decode(logo_str)
                with open(target_path, "wb") as f:
                    f.write(raw)
                return True
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                try:
                    if os.path.exists(target_path):
                        os.remove(target_path)
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    pass
                return False
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return False

    # ------------------------------------------------------------------
    def load_printers(self):
        with self._storage_lock:
            try:
                data = self._read_printers_payload(self.file_path)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
                self.log_warning(f"printers.json rusak/tidak terbaca, coba recovery backup: {exc}")
                data = self._recover_printers_payload()
        printers = data.get('printers', [])
        normalized, changed = self._normalize_printers(printers)
        if changed:
            self.save_printers(normalized)
        return normalized

    def save_printers(self, printers):
        payload = {"printers": printers if isinstance(printers, list) else []}
        with self._storage_lock:
            self._atomic_write_json(self.file_path, payload)
            self._atomic_write_json(self.file_backup_path, payload)

    # ------------------------------------------------------------------
    def add_printer(self, printer):
        printers = self.load_printers()
        printers.append(printer)
        self.save_printers(printers)

    def remove_printer(self, index):
        printers = self.load_printers()
        if 0 <= index < len(printers):
            printers.pop(index)
            self.save_printers(printers)

    def set_default(self, index):
        printers = self.load_printers()
        for i, p in enumerate(printers):
            p['default'] = i == index
            p['is_default'] = 1 if p['default'] else 0
        self.save_printers(printers)

    def get_app_default_printer_name(self) -> str:
        for p in self.load_printers():
            if p.get("default"):
                return str(p.get("name") or "").strip()
        return ""

    def _normalize_printers(self, printers):
        changed = False
        max_id = 0
        default_found = False
        for p in printers:
            try:
                max_id = max(max_id, int(p.get("id") or 0))
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                pass
        next_id = max_id + 1
        normalized = []
        for p in printers:
            rec = dict(p or {})
            # map legacy -> new
            if not rec.get("name") and rec.get("nama"):
                rec["name"] = rec.get("nama")
                changed = True
            if not rec.get("paper_size") and rec.get("lebar_kertas"):
                rec["paper_size"] = rec.get("lebar_kertas")
                changed = True
            if "default" not in rec and "is_default" in rec:
                try:
                    rec["default"] = bool(int(rec.get("is_default") or 0))
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    rec["default"] = False
                changed = True
            if "id" not in rec:
                rec["id"] = next_id
                next_id += 1
                changed = True
            # sync new -> legacy
            if rec.get("name") and rec.get("nama") != rec.get("name"):
                rec["nama"] = rec.get("name")
                changed = True
            if rec.get("paper_size") and rec.get("lebar_kertas") != rec.get("paper_size"):
                rec["lebar_kertas"] = rec.get("paper_size")
                changed = True
            rec["default"] = bool(rec.get("default"))
            if rec["default"]:
                if default_found:
                    rec["default"] = False
                    changed = True
                else:
                    default_found = True
            if rec.get("is_default") != (1 if rec["default"] else 0):
                rec["is_default"] = 1 if rec["default"] else 0
                changed = True
            normalized.append(rec)
        return normalized, changed
