import os
import threading
import sqlite3
from datetime import datetime
import logging

from pypos.core.utils.config_utils import read_config
from pypos.core.utils.path_utils import get_db_path
from pypos.core.utils.sql_identifier_utils import quote_sql_identifier
from pypos.core.utils.sql_query_builder import render_sql_template

LOGGER = logging.getLogger(__name__)
_NUMERIC_PARSE_EXCEPTIONS = (TypeError, ValueError)
_MIGRATION_EXCEPTIONS = (sqlite3.Error, OSError, RuntimeError, ValueError, TypeError)

TARGET_SCHEMA_VERSION = 5

_MIGRATION_LOCK = threading.Lock()
_MIGRATED_DB_ONCE = set()


def _normalize_db_path(db_path=None):
    return os.path.abspath(db_path or get_db_path())


# edited by glg
def _quote_identifier(name):
    return quote_sql_identifier(name, strict=True)


def _connect_for_migration(db_path):
    from pypos.core.utils.db_helper import open_sqlite_connection

    config = read_config()
    try:
        timeout = int(config.get("sqlite_timeout", 30))
    except _NUMERIC_PARSE_EXCEPTIONS:
        timeout = 30
    conn = open_sqlite_connection(
        db_path=db_path,
        timeout=timeout,
        uri=False,
        app_settings=config,
        apply_pragmas=False,
        ensure_indexes=False,
        run_migrations=False,
    )
    try:
        if int(config.get("sqlite_foreign_keys", 1)) == 1:
            conn.execute("PRAGMA foreign_keys = ON")
        if int(config.get("sqlite_use_wal", 1)) == 1:
            conn.execute("PRAGMA journal_mode = WAL")
        busy_ms = int(config.get("sqlite_busy_timeout_ms", 5000))
        if busy_ms > 0:
            conn.execute(f"PRAGMA busy_timeout = {busy_ms}")
    except (sqlite3.Error, TypeError, ValueError) as e:
        LOGGER.warning("Schema migrator gagal set PRAGMA: %s", e)
    return conn


def _ensure_schema_version_table(conn):
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS schema_version (
            id INTEGER PRIMARY KEY CHECK (id = 1),
            version INTEGER NOT NULL DEFAULT 0,
            updated_at TEXT NOT NULL
        )
        """
    )
    cur = conn.cursor()
    cur.execute("SELECT COUNT(1) FROM schema_version WHERE id = 1")
    exists = int(cur.fetchone()[0] or 0) > 0
    if not exists:
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        cur.execute(
            "INSERT INTO schema_version (id, version, updated_at) VALUES (1, 0, ?)",
            (now,),
        )
    conn.commit()
    cur.close()


def get_schema_version(db_path=None):
    db_file = _normalize_db_path(db_path)
    conn = _connect_for_migration(db_file)
    try:
        _ensure_schema_version_table(conn)
        cur = conn.cursor()
        cur.execute("SELECT version FROM schema_version WHERE id = 1")
        row = cur.fetchone()
        cur.close()
        if not row:
            return 0
        return int(row[0] or 0)
    finally:
        conn.close()


def _ensure_column(cursor, table_name, column_name, column_sql):
    table_sql = _quote_identifier(table_name)
    column_sql_name = _quote_identifier(column_name)
    cursor.execute(f"PRAGMA table_info({table_sql})")
    cols = {str(row[1]) for row in cursor.fetchall() if row and len(row) > 1}
    if column_name not in cols:
        cursor.execute(f"ALTER TABLE {table_sql} ADD COLUMN {column_sql_name} {column_sql}")


def _table_exists(cursor, table_name):
    cursor.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
        (table_name,),
    )
    return cursor.fetchone() is not None


def _table_has_columns(cursor, table_name, required_columns):
    if not _table_exists(cursor, table_name):
        return False
    table_sql = _quote_identifier(table_name)
    try:
        cursor.execute(f"PRAGMA table_info({table_sql})")
        cols = {str(row[1]) for row in cursor.fetchall() if row and len(row) > 1}
    except (sqlite3.Error, ValueError):
        return False
    return set(required_columns or ()).issubset(cols)


# edited by glg
# Hardening migrasi index:
# - skip aman jika tabel/kolom belum ada (legacy DB)
# - hindari migrasi gagal total hanya karena 1 index.
def _create_index_if_columns_exist(cursor, table_name, index_name, columns):
    cols = tuple(str(col or "").strip() for col in (columns or ()))
    if not cols:
        return False
    try:
        table_sql = _quote_identifier(table_name)
        index_sql = _quote_identifier(index_name)
        cols_sql = ", ".join([_quote_identifier(col) for col in cols])
    except ValueError:
        LOGGER.warning(
            "Schema migrator skip index %s: identifier tidak valid pada tabel %s",
            str(index_name or ""),
            str(table_name or ""),
        )
        return False
    if not _table_has_columns(cursor, table_name, cols):
        LOGGER.warning(
            "Schema migrator skip index %s: kolom belum lengkap pada tabel %s",
            str(index_name or ""),
            str(table_name or ""),
        )
        return False
    cursor.execute(
        f"CREATE INDEX IF NOT EXISTS {index_sql} ON {table_sql}({cols_sql})"
    )
    return True


def _migration_v1(cursor):
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS app_state (
            key TEXT PRIMARY KEY,
            value TEXT,
            updated_at TEXT
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS sync_tracking (
            tabel TEXT PRIMARY KEY,
            last_update TEXT,
            last_id INTEGER
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS settlement_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            tanggal TEXT,
            admin TEXT,
            kasir TEXT,
            total_harus REAL DEFAULT 0,
            total_disetor REAL DEFAULT 0,
            total_non_tunai REAL DEFAULT 0,
            total_refund_cash REAL DEFAULT 0,
            selisih REAL DEFAULT 0,
            status TEXT,
            data_transaksi_id TEXT
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS return_transaksi_penjualan (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            transaksi_id TEXT NOT NULL,
            customer_id TEXT NULL,
            tanggal_return TEXT NOT NULL,
            total_return REAL NOT NULL DEFAULT 0,
            kode_voucher TEXT UNIQUE NOT NULL,
            nilai_voucher REAL NOT NULL DEFAULT 0,
            keterangan TEXT NULL,
            jenis_return TEXT NULL DEFAULT 'partial',
            refund_method TEXT NULL,
            refund_amount REAL NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS detail_return_transaksi_penjualan (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            return_id INTEGER NOT NULL,
            produk_id TEXT NOT NULL,
            produk_nama TEXT NULL,
            jumlah INTEGER NOT NULL DEFAULT 0,
            jenis_return TEXT NULL DEFAULT 'partial',
            harga REAL NOT NULL DEFAULT 0,
            subtotal REAL NOT NULL DEFAULT 0
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS voucher_return (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            return_id INTEGER NULL,
            transaksi_id TEXT NULL,
            customer_id TEXT NULL,
            kode TEXT UNIQUE NOT NULL,
            nilai REAL DEFAULT 0,
            nilai_awal REAL NOT NULL DEFAULT 0,
            saldo REAL NOT NULL DEFAULT 0,
            is_used INTEGER DEFAULT 0,
            status TEXT NOT NULL DEFAULT 'aktif',
            dtime_terbit TEXT NOT NULL,
            dtime_expired TEXT NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS voucher_usage (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            voucher_id INTEGER NOT NULL,
            transaksi_id TEXT NOT NULL,
            nilai_pakai REAL NOT NULL DEFAULT 0,
            dtime TEXT NOT NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS pembatalan_transaksi_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            transaksi_id TEXT NOT NULL,
            nomer TEXT,
            transaksi_dtime TEXT,
            customers_nama TEXT,
            kasir_nama TEXT,
            transaksi_nilai REAL,
            admin_verifikasi TEXT NOT NULL,
            dibatalkan_oleh_id TEXT,
            dibatalkan_oleh_nama TEXT,
            cancel_dtime TEXT NOT NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS export_flux (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            batch_start TEXT NOT NULL,
            batch_end TEXT NOT NULL,
            table_name TEXT NULL,
            server_hash TEXT NULL,
            file_seq INTEGER NULL,
            file_hash TEXT NULL,
            file_size INTEGER NULL,
            row_count INTEGER NOT NULL DEFAULT 0,
            status TEXT NOT NULL DEFAULT 'PENDING',
            file_path TEXT NULL,
            error_log TEXT NULL,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS export_retry_state (
            table_name TEXT NOT NULL,
            server_hash TEXT NOT NULL,
            attempt_count INTEGER NOT NULL DEFAULT 0,
            next_retry_at TEXT NULL,
            error_code TEXT NULL,
            last_error TEXT NULL,
            updated_at TEXT NOT NULL,
            PRIMARY KEY (table_name, server_hash)
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS export_cursor_table (
            table_name TEXT PRIMARY KEY,
            last_id INTEGER NOT NULL,
            server_hash TEXT NULL
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS export_cursor_table_scoped (
            table_name TEXT NOT NULL,
            server_hash TEXT NOT NULL,
            last_id INTEGER NOT NULL,
            PRIMARY KEY (table_name, server_hash)
        )
        """
    )
    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS export_file_seq (
            id INTEGER PRIMARY KEY CHECK (id = 1),
            last_no INTEGER NOT NULL
        )
        """
    )

    _ensure_column(cursor, "return_transaksi_penjualan", "customer_id", "TEXT")
    _ensure_column(cursor, "return_transaksi_penjualan", "keterangan", "TEXT")
    _ensure_column(cursor, "return_transaksi_penjualan", "jenis_return", "TEXT DEFAULT 'partial'")
    _ensure_column(cursor, "return_transaksi_penjualan", "refund_method", "TEXT")
    _ensure_column(cursor, "return_transaksi_penjualan", "refund_amount", "REAL")
    _ensure_column(cursor, "detail_return_transaksi_penjualan", "subtotal", "REAL NOT NULL DEFAULT 0")
    _ensure_column(cursor, "voucher_return", "nilai_awal", "REAL NOT NULL DEFAULT 0")
    _ensure_column(cursor, "voucher_return", "saldo", "REAL NOT NULL DEFAULT 0")
    _ensure_column(cursor, "voucher_return", "status", "TEXT NOT NULL DEFAULT 'aktif'")
    _ensure_column(cursor, "voucher_return", "dtime_terbit", "TEXT")
    _ensure_column(cursor, "voucher_return", "dtime_expired", "TEXT")
    _ensure_column(cursor, "voucher_return", "return_id", "INTEGER")
    _ensure_column(cursor, "voucher_return", "transaksi_id", "TEXT")
    _ensure_column(cursor, "voucher_return", "customer_id", "TEXT")
    _ensure_column(cursor, "settlement_history", "kasir", "TEXT")
    _ensure_column(cursor, "settlement_history", "total_non_tunai", "REAL DEFAULT 0")
    _ensure_column(cursor, "settlement_history", "total_refund_cash", "REAL DEFAULT 0")
    _ensure_column(cursor, "settlement_history", "data_transaksi_id", "TEXT")
    _ensure_column(cursor, "export_flux", "attempt_count", "INTEGER")
    _ensure_column(cursor, "export_flux", "next_retry_at", "TEXT")
    _ensure_column(cursor, "export_flux", "error_code", "TEXT")

    cursor.execute(
        "INSERT OR IGNORE INTO export_file_seq (id, last_no) VALUES (1, 0)"
    )

    _create_index_if_columns_exist(
        cursor,
        "return_transaksi_penjualan",
        "idx_return_transaksi_id",
        ("transaksi_id",),
    )
    _create_index_if_columns_exist(
        cursor,
        "return_transaksi_penjualan",
        "idx_return_tanggal",
        ("tanggal_return",),
    )
    _create_index_if_columns_exist(
        cursor,
        "detail_return_transaksi_penjualan",
        "idx_detail_return_return_id",
        ("return_id",),
    )
    _create_index_if_columns_exist(
        cursor,
        "detail_return_transaksi_penjualan",
        "idx_detail_return_return_produk",
        ("return_id", "produk_id"),
    )
    _create_index_if_columns_exist(
        cursor,
        "voucher_return",
        "idx_voucher_return_kode",
        ("kode",),
    )
    _create_index_if_columns_exist(
        cursor,
        "voucher_usage",
        "idx_voucher_usage_transaksi",
        ("transaksi_id",),
    )
    _create_index_if_columns_exist(
        cursor,
        "pembatalan_transaksi_history",
        "idx_pembatalan_history_transaksi",
        ("transaksi_id",),
    )
    _create_index_if_columns_exist(
        cursor,
        "pembatalan_transaksi_history",
        "idx_pembatalan_history_cancel_dtime",
        ("cancel_dtime",),
    )
    _create_index_if_columns_exist(
        cursor,
        "settlement_history",
        "idx_settlement_history_tanggal",
        ("tanggal",),
    )
    _create_index_if_columns_exist(
        cursor,
        "export_retry_state",
        "idx_export_retry_state_next_retry",
        ("next_retry_at",),
    )
    _create_index_if_columns_exist(
        cursor,
        "produk",
        "idx_produk_barcode",
        ("barcode",),
    )
    _create_index_if_columns_exist(
        cursor,
        "produk",
        "idx_produk_nama",
        ("nama",),
    )
    _create_index_if_columns_exist(
        cursor,
        "per_customers",
        "idx_per_customers_nama",
        ("nama",),
    )
    # edited by glg
    # Hardening lookup device->branch agar sinkron/export lebih stabil di data besar.
    _create_index_if_columns_exist(
        cursor,
        "per_cabang_device",
        "idx_per_cabang_device_machine_status_trash_hash",
        ("machine_id", "status", "trash", "server_hash"),
    )


def _migration_v2(cursor):
    if not _table_exists(cursor, "voucher_return"):
        return

    cursor.execute('PRAGMA table_info("voucher_return")')
    cols = {str(row[1]) for row in cursor.fetchall() if row and len(row) > 1}

    if {"nilai", "nilai_awal"}.issubset(cols):
        cursor.execute(
            """
            UPDATE voucher_return
            SET nilai_awal = nilai
            WHERE (nilai_awal IS NULL OR nilai_awal = 0)
              AND nilai IS NOT NULL
            """
        )
    if {"nilai", "saldo"}.issubset(cols):
        cursor.execute(
            """
            UPDATE voucher_return
            SET saldo = nilai
            WHERE (saldo IS NULL OR saldo = 0)
              AND nilai IS NOT NULL
            """
        )
    if {"is_used", "status"}.issubset(cols):
        cursor.execute(
            """
            UPDATE voucher_return
            SET status = CASE WHEN COALESCE(is_used, 0) = 1 THEN 'habis' ELSE 'aktif' END
            WHERE status IS NULL OR status = ''
            """
        )


def _migration_v3(cursor):
    # edited by glg
    # Kolom eksplisit point transaksi untuk konsistensi print/reprint/export.
    # Guard tabel transaksi mungkin belum ada di DB baru/minimal.
    if not _table_exists(cursor, "transaksi"):
        return
    _ensure_column(cursor, "transaksi", "point_transaksi", "INTEGER DEFAULT 0")


def _migration_v4(cursor):
    # edited by glg
    # Hardening mode PPN agar settlement/export dapat audit include vs exclude secara eksplisit.
    if not _table_exists(cursor, "transaksi"):
        return
    _ensure_column(cursor, "transaksi", "ppn_mode", "TEXT DEFAULT 'include'")

    cursor.execute('PRAGMA table_info("transaksi")')
    cols = {str(row[1]) for row in cursor.fetchall() if row and len(row) > 1}
    has_diskon_log = "diskon_log" in cols

    if has_diskon_log:
        cursor.execute(
            """
            UPDATE transaksi
            SET ppn_mode = 'include'
            WHERE LOWER(COALESCE(diskon_log, '')) LIKE '%ppn_mode=include%'
            """
        )
        cursor.execute(
            """
            UPDATE transaksi
            SET ppn_mode = 'exclude'
            WHERE LOWER(COALESCE(diskon_log, '')) LIKE '%ppn_mode=exclude%'
            """
        )

    cursor.execute(
        """
        UPDATE transaksi
        SET ppn_mode = CASE
            WHEN LOWER(COALESCE(ppn_mode, '')) = 'include' THEN 'include'
            WHEN LOWER(COALESCE(ppn_mode, '')) = 'exclude' THEN 'exclude'
            ELSE 'include'
        END
        """
    )


def _migration_v5(cursor):
    # edited by glg
    # Cleanup schema destruktif terkontrol:
    # - hapus kolom legacy voucher_return.nilai
    # - tetap preserve data inti via create-copy-swap (SQLite-safe)
    if not _table_exists(cursor, "voucher_return"):
        return

    cursor.execute('PRAGMA table_info("voucher_return")')
    source_cols = [str(row[1]) for row in cursor.fetchall() if row and len(row) > 1]
    source_col_set = set(source_cols)
    if "nilai" not in source_col_set:
        return

    temp_table = "voucher_return__legacy_v5"
    temp_table_sql = _quote_identifier(temp_table)
    voucher_return_sql = _quote_identifier("voucher_return")
    if _table_exists(cursor, temp_table):
        cursor.execute(f"DROP TABLE {temp_table_sql}")

    cursor.execute(f"ALTER TABLE {voucher_return_sql} RENAME TO {temp_table_sql}")
    cursor.execute(
        """
        CREATE TABLE "voucher_return" (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            return_id INTEGER NULL,
            transaksi_id TEXT NULL,
            customer_id TEXT NULL,
            kode TEXT UNIQUE NOT NULL,
            nilai_awal REAL NOT NULL DEFAULT 0,
            saldo REAL NOT NULL DEFAULT 0,
            is_used INTEGER DEFAULT 0,
            status TEXT NOT NULL DEFAULT 'aktif',
            dtime_terbit TEXT NOT NULL,
            dtime_expired TEXT NULL
        )
        """
    )

    def _expr(col_name, fallback_expr):
        if col_name in source_col_set:
            return f'"{col_name}"'
        return fallback_expr

    if "nilai_awal" in source_col_set and "nilai" in source_col_set:
        nilai_awal_expr = (
            'CASE '
            'WHEN COALESCE("nilai_awal", 0) <= 0 AND COALESCE("nilai", 0) > 0 THEN "nilai" '
            'ELSE COALESCE("nilai_awal", 0) END'
        )
    elif "nilai_awal" in source_col_set:
        nilai_awal_expr = 'COALESCE("nilai_awal", 0)'
    elif "nilai" in source_col_set:
        nilai_awal_expr = 'COALESCE("nilai", 0)'
    else:
        nilai_awal_expr = "0"

    if "saldo" in source_col_set and "nilai" in source_col_set:
        saldo_expr = (
            'CASE '
            'WHEN COALESCE("saldo", 0) <= 0 AND COALESCE("nilai", 0) > 0 THEN "nilai" '
            'ELSE COALESCE("saldo", 0) END'
        )
    elif "saldo" in source_col_set:
        saldo_expr = 'COALESCE("saldo", 0)'
    elif "nilai" in source_col_set:
        saldo_expr = 'COALESCE("nilai", 0)'
    else:
        saldo_expr = "0"

    if "status" in source_col_set and "is_used" in source_col_set:
        status_expr = (
            'CASE '
            'WHEN COALESCE("status","") <> "" THEN "status" '
            'WHEN COALESCE("is_used",0) = 1 THEN \'habis\' '
            'ELSE \'aktif\' END'
        )
    elif "status" in source_col_set:
        status_expr = 'CASE WHEN COALESCE("status","") <> "" THEN "status" ELSE \'aktif\' END'
    elif "is_used" in source_col_set:
        status_expr = 'CASE WHEN COALESCE("is_used",0) = 1 THEN \'habis\' ELSE \'aktif\' END'
    else:
        status_expr = "'aktif'"

    select_sql = render_sql_template(
        """
        INSERT INTO "voucher_return" (
            id,
            return_id,
            transaksi_id,
            customer_id,
            kode,
            nilai_awal,
            saldo,
            is_used,
            status,
            dtime_terbit,
            dtime_expired
        )
        SELECT
            {id_expr} AS id,
            {return_id_expr} AS return_id,
            {transaksi_id_expr} AS transaksi_id,
            {customer_id_expr} AS customer_id,
            {kode_expr} AS kode,
            {nilai_awal_expr} AS nilai_awal,
            {saldo_expr} AS saldo,
            {is_used_expr} AS is_used,
            {status_expr} AS status,
            {dtime_terbit_expr} AS dtime_terbit,
            {dtime_expired_expr} AS dtime_expired
        FROM {temp_table_sql}
        """,
        id_expr=_expr("id", "NULL"),
        return_id_expr=_expr("return_id", "NULL"),
        transaksi_id_expr=_expr("transaksi_id", "NULL"),
        customer_id_expr=_expr("customer_id", "NULL"),
        kode_expr=_expr("kode", "''"),
        nilai_awal_expr=nilai_awal_expr,
        saldo_expr=saldo_expr,
        is_used_expr=_expr("is_used", "0"),
        status_expr=status_expr,
        dtime_terbit_expr=_expr("dtime_terbit", "datetime('now')"),
        dtime_expired_expr=_expr("dtime_expired", "NULL"),
        temp_table_sql=temp_table_sql,
    )
    cursor.execute(select_sql)
    cursor.execute(f"DROP TABLE {temp_table_sql}")  # nosec B608

    _create_index_if_columns_exist(
        cursor,
        "voucher_return",
        "idx_voucher_return_kode",
        ("kode",),
    )


_MIGRATIONS = {
    1: _migration_v1,
    2: _migration_v2,
    3: _migration_v3,
    4: _migration_v4,
    5: _migration_v5,
}


def run_schema_migrations(db_path=None, strict=False):
    db_file = _normalize_db_path(db_path)
    conn = None
    try:
        conn = _connect_for_migration(db_file)
        _ensure_schema_version_table(conn)
        cur = conn.cursor()
        cur.execute("SELECT version FROM schema_version WHERE id = 1")
        row = cur.fetchone()
        current_version = int(row[0] or 0) if row else 0

        if current_version >= TARGET_SCHEMA_VERSION:
            cur.close()
            return True

        for next_version in range(current_version + 1, TARGET_SCHEMA_VERSION + 1):
            migration = _MIGRATIONS.get(next_version)
            if migration is None:
                raise RuntimeError(f"Migrasi schema versi {next_version} tidak ditemukan.")
            migration(cur)
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            cur.execute(
                "UPDATE schema_version SET version = ?, updated_at = ? WHERE id = 1",
                (next_version, now),
            )
            conn.commit()
            LOGGER.info("Schema migration sukses ke versi %s", next_version)

        cur.close()
        return True
    except _MIGRATION_EXCEPTIONS as e:
        if conn is not None:
            try:
                conn.rollback()
            except sqlite3.Error:
                pass
        LOGGER.error("Schema migration gagal: %s", e, exc_info=True)
        if strict:
            raise
        return False
    finally:
        if conn is not None:
            conn.close()


def run_schema_migrations_once(db_path=None, strict=False):
    db_file = _normalize_db_path(db_path)
    with _MIGRATION_LOCK:
        if db_file in _MIGRATED_DB_ONCE:
            return True
        ok = run_schema_migrations(db_file, strict=strict)
        if ok:
            _MIGRATED_DB_ONCE.add(db_file)
        return ok
