package com.fieldlabel.collector.service

import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.brother.sdk.lmprinter.Channel
import com.brother.sdk.lmprinter.OpenChannelError
import com.brother.sdk.lmprinter.PrintError
import com.brother.sdk.lmprinter.PrinterDriverGenerator
import com.brother.sdk.lmprinter.PrinterModel
import com.brother.sdk.lmprinter.setting.PTPrintSettings
import com.fieldlabel.collector.domain.model.CollectionRecord
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PrinterService @Inject constructor(
    @ApplicationContext private val context: Context
) {
    companion object {
        // テープ幅に応じたラベル高さ (dots, 180dpi 印字幅)
        val TAPE_HEIGHT_DOTS = mapOf(12 to 54, 18 to 90, 24 to 128, 36 to 182)
        // 180dpi = 7.09 dots/mm
        const val DOTS_PER_MM = 7.09f
        fun mmToDots(mm: Int): Int = (mm * DOTS_PER_MM).toInt()
    }

    // ── プリンター設定 ──────────────────────────────────────────────────
    var printerLocalName: String    = ""
    var printerLmModel: PrinterModel = PrinterModel.PT_P910BT

    // ── ラベル設定 ──────────────────────────────────────────────────────
    /** レイアウト: "simple_h" | "column_h" | "simple_v" | "column_v" | "auto_v" | "auto_h" */
    var labelLayout:   String = "simple_h"
    var labelLengthMm: Int    = 70
    var labelFontScale: Float = 1.0f
    var cutMode:       String = "auto"

    // ── 要素の表示フラグ ────────────────────────────────────────────────
    var showQr:          Boolean = false
    var showDate:        Boolean = true
    var showLocation:    Boolean = true
    var showNotes:       Boolean = true
    var showCollector:   Boolean = true
    var showCoordinates: Boolean = false

    // ── 要素ごとの文字スケール（ベースサイズに対する倍率）───────────────
    var elemIdScale:         Float = 1.2f
    var elemDateScale:       Float = 0.85f
    var elemLocationScale:   Float = 0.85f
    var elemCollectorScale:  Float = 0.85f
    var elemNotesScale:      Float = 0.8f

    // ── 要素ごとの占有比（セクション配置での幅/高さの相対比率）──────────
    var elemIdWeight:         Float = 1.5f
    var elemDateWeight:       Float = 1.0f
    var elemLocationWeight:   Float = 1.2f
    var elemCollectorWeight:  Float = 1.0f
    var elemNotesWeight:      Float = 1.0f
    var elemQrWeight:         Float = 1.5f

    /** フォントファミリー名（空文字列 = Android デフォルト）*/
    var labelFontFamily: String  = ""
    /** 全要素をボールド体で描画する */
    var labelFontBold:   Boolean = false

    // ════════════════════════════════════════════════════════════════════
    // Bluetooth / 印刷ロジック（変更なし）
    // ════════════════════════════════════════════════════════════════════

    private fun checkBluetooth(): PrintResult? {
        val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
        val adapter = manager?.adapter
        return when {
            adapter == null    -> PrintResult.Error("Bluetooth アダプターを取得できません\nBluetoothが搭載されているか確認してください")
            !adapter.isEnabled -> PrintResult.Error("Bluetooth がオフです\nBluetoothをオンにしてから再度お試しください")
            else               -> null
        }
    }

    suspend fun printLabel(record: CollectionRecord, tapeWidthMm: Int): PrintResult {
        if (printerLocalName.isBlank())
            return PrintResult.Error("プリンターが設定されていません\n設定画面でプリンターを選択してください")
        checkBluetooth()?.let { return it }
        val bitmap = buildLabelBitmap(record, tapeWidthMm)
        return withContext(Dispatchers.IO) { printWithNewSdk(bitmap, tapeWidthMm) }
    }

    suspend fun printLocationLabel(record: CollectionRecord, tapeWidthMm: Int): PrintResult {
        if (printerLocalName.isBlank())
            return PrintResult.Error("プリンターが設定されていません\n設定画面でプリンターを選択してください")
        checkBluetooth()?.let { return it }
        val bitmap = buildLocationLabelBitmap(record, tapeWidthMm)
        return withContext(Dispatchers.IO) { printWithNewSdk(bitmap, tapeWidthMm) }
    }

    /**
     * 複数の標本ラベルをバッチ印刷する。
     *
     * 各ラベルを独立したクロージャとして printImageWithClosures に渡すことで、
     * SDK がページ境界ごとにカット命令（autoCutForEachPageCount=1）を発行する。
     * これにより各ラベルの末尾でフルカット／ハーフカット（cutMode 設定による）が行われる。
     */
    suspend fun printLabelsAsBatch(records: List<CollectionRecord>, tapeWidthMm: Int): PrintResult {
        if (records.isEmpty()) return PrintResult.Error("印刷対象の標本がありません")
        if (printerLocalName.isBlank())
            return PrintResult.Error("プリンターが設定されていません\n設定画面でプリンターを選択してください")
        checkBluetooth()?.let { return it }
        val bitmaps = records.map { buildLabelBitmap(it, tapeWidthMm) }
        return withContext(Dispatchers.IO) { printBatchWithNewSdk(bitmaps, tapeWidthMm) }
    }

    /**
     * 複数の地点ラベルをバッチ印刷する。
     * printLabelsAsBatch と同じ規則（printImageWithClosures + autoCutForEachPageCount=1）。
     */
    suspend fun printLocationLabelsAsBatch(records: List<CollectionRecord>, tapeWidthMm: Int): PrintResult {
        if (records.isEmpty()) return PrintResult.Error("印刷対象がありません")
        if (printerLocalName.isBlank())
            return PrintResult.Error("プリンターが設定されていません\n設定画面でプリンターを選択してください")
        checkBluetooth()?.let { return it }
        val bitmaps = records.map { buildLocationLabelBitmap(it, tapeWidthMm) }
        return withContext(Dispatchers.IO) { printBatchWithNewSdk(bitmaps, tapeWidthMm) }
    }

    suspend fun printTest(tapeWidthMm: Int): PrintResult {
        if (printerLocalName.isBlank())
            return PrintResult.Error("プリンターが設定されていません\n設定画面でプリンターを選択してください")
        checkBluetooth()?.let { return it }
        val bitmap = buildTestBitmap(tapeWidthMm)
        return withContext(Dispatchers.IO) { printWithNewSdk(bitmap, tapeWidthMm) }
    }

    private fun stripAndroidSuffix(name: String): String =
        name.replace(Regex("^(PT-[A-Z0-9]+BT)\\d{4,}$"), "$1")

    @SuppressLint("MissingPermission")
    private fun findBondedMac(adapter: android.bluetooth.BluetoothAdapter): String? {
        val normalized = stripAndroidSuffix(printerLocalName)
        return try {
            adapter.bondedDevices?.firstOrNull { device ->
                val dn = device.name ?: return@firstOrNull false
                dn == printerLocalName || dn == normalized || stripAndroidSuffix(dn) == normalized
            }?.address
        } catch (_: Exception) { null }
    }

    @SuppressLint("MissingPermission")
    private fun buildChannelsToTry(adapter: android.bluetooth.BluetoothAdapter): List<Channel> = buildList {
        val mac = findBondedMac(adapter)
        if (mac != null) add(Channel.newBluetoothChannel(mac, adapter))
        add(Channel.newBluetoothLowEnergyChannel(printerLocalName, context, adapter))
        val stripped = stripAndroidSuffix(printerLocalName)
        if (stripped != printerLocalName)
            add(Channel.newBluetoothLowEnergyChannel(stripped, context, adapter))
    }

    /**
     * 複数ビットマップを 1 接続でバッチ印刷する。
     *
     * printImageWithClosures は各クロージャを「1ページ」として扱い、
     * autoCutForEachPageCount=1 と組み合わせることでページごとにカットを実行する。
     *
     * @return 接続失敗時は null（次のチャンネルへフォールバック）
     */
    private fun tryPrintBatchWithChannel(
        channel: Channel,
        bitmaps: List<Bitmap>,
        tapeWidthMm: Int
    ): PrintResult? {
        return try {
            val generateResult = PrinterDriverGenerator.openChannel(channel)
            if (generateResult.error.code != OpenChannelError.ErrorCode.NoError) return null
            val driver = generateResult.driver
            try {
                val settings = PTPrintSettings(printerLmModel).apply {
                    labelSize               = tapeMmToLabelSize(tapeWidthMm)
                    isAutoCut               = cutMode != "none"
                    isHalfCut              = cutMode == "half"
                    // 全ラベル枚数分送り終えた後に1回だけカット
                    // → 全ラベルが1本のテープとして繰り出され、末尾でカットされる
                    autoCutForEachPageCount = bitmaps.size
                    workPath                = context.cacheDir.absolutePath
                }
                // 各ビットマップをクロージャとして登録（ラムダ）
                val closures = ArrayList<Function0<Bitmap>>(bitmaps.size)
                bitmaps.forEach { bmp -> closures.add { bmp } }
                val printError = driver.printImageWithClosures(closures, settings)
                if (printError.code == PrintError.ErrorCode.NoError) {
                    PrintResult.Success("印刷完了")
                } else {
                    val logs = printError.allLogs.joinToString("\n  ") { it.toString() }
                    PrintResult.Error(
                        "印刷失敗\n  エラーコード: ${printError.code}" +
                        (if (logs.isNotBlank()) "\n  ログ:\n  $logs" else "")
                    )
                }
            } finally { driver.closeChannel() }
        } catch (_: Exception) { null }
    }

    @SuppressLint("MissingPermission")
    private fun printBatchWithNewSdk(bitmaps: List<Bitmap>, tapeWidthMm: Int): PrintResult {
        val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
            ?: return PrintResult.Error("Bluetooth マネージャーを取得できません")
        val adapter = manager.adapter
            ?: return PrintResult.Error("Bluetooth アダプターを取得できません\nBluetoothをオンにしてください")
        for (channel in buildChannelsToTry(adapter)) {
            val result = tryPrintBatchWithChannel(channel, bitmaps, tapeWidthMm)
            if (result != null) return result
        }
        return PrintResult.Error(
            "プリンターへの接続に失敗しました\n\n確認事項:\n" +
            "• プリンターの電源が入っているか\n• Bluetooth がオンになっているか\n" +
            "• 設定画面でプリンター名 \"$printerLocalName\" が正しいか確認してください"
        )
    }

    private fun tryPrintWithChannel(channel: Channel, bitmap: Bitmap, tapeWidthMm: Int): PrintResult? {
        return try {
            val generateResult = PrinterDriverGenerator.openChannel(channel)
            if (generateResult.error.code != OpenChannelError.ErrorCode.NoError) return null
            val driver = generateResult.driver
            try {
                val settings = PTPrintSettings(printerLmModel).apply {
                    labelSize = tapeMmToLabelSize(tapeWidthMm)
                    isAutoCut = cutMode != "none"
                    isHalfCut = cutMode == "half"
                    workPath  = context.cacheDir.absolutePath
                }
                val printError = driver.printImage(bitmap, settings)
                if (printError.code == PrintError.ErrorCode.NoError) {
                    PrintResult.Success("印刷完了")
                } else {
                    val logs = printError.allLogs.joinToString("\n  ") { it.toString() }
                    PrintResult.Error(
                        "印刷失敗\n  エラーコード: ${printError.code}" +
                        (if (logs.isNotBlank()) "\n  ログ:\n  $logs" else "")
                    )
                }
            } finally { driver.closeChannel() }
        } catch (_: Exception) { null }
    }

    @SuppressLint("MissingPermission")
    private fun printWithNewSdk(bitmap: Bitmap, tapeWidthMm: Int): PrintResult {
        val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
            ?: return PrintResult.Error("Bluetooth マネージャーを取得できません")
        val adapter = manager.adapter
            ?: return PrintResult.Error("Bluetooth アダプターを取得できません\nBluetoothをオンにしてください")
        for (channel in buildChannelsToTry(adapter)) {
            val result = tryPrintWithChannel(channel, bitmap, tapeWidthMm)
            if (result != null) return result
        }
        return PrintResult.Error(
            "プリンターへの接続に失敗しました\n\n確認事項:\n" +
            "• プリンターの電源が入っているか\n• Bluetooth がオンになっているか\n" +
            "• 設定画面でプリンター名 \"$printerLocalName\" が正しいか確認してください"
        )
    }

    private fun tapeMmToLabelSize(mm: Int): PTPrintSettings.LabelSize = when (mm) {
        12   -> PTPrintSettings.LabelSize.Width12mm
        18   -> PTPrintSettings.LabelSize.Width18mm
        24   -> PTPrintSettings.LabelSize.Width24mm
        36   -> PTPrintSettings.LabelSize.Width36mm
        else -> PTPrintSettings.LabelSize.Width24mm
    }

    // ════════════════════════════════════════════════════════════════════
    // ビットマップ生成（公開 API）
    // ════════════════════════════════════════════════════════════════════

    /** 標本ラベルビットマップを生成する */
    fun buildLabelBitmap(record: CollectionRecord, tapeWidthMm: Int): Bitmap {
        if (labelLayout == "auto_v") return buildAutoPortrait(tapeWidthMm, record.specimenId, record)
        if (labelLayout == "auto_h") return buildAutoLandscape(tapeWidthMm, record.specimenId, record)
        val h = TAPE_HEIGHT_DOTS[tapeWidthMm] ?: 128
        val w = mmToDots(labelLengthMm).coerceAtLeast(mmToDots(30))
        val isV = labelLayout == "simple_v" || labelLayout == "column_v"
        val base = when (labelLayout) {
            "column_h", "column_v" -> buildColumnH(w, h, record.specimenId, record, isVertical = isV)
            "row_h",    "row_v"    -> buildRowH   (w, h, record.specimenId, record)
            else                   -> buildSimpleH(w, h, record.specimenId, record, isVertical = isV)
        }
        return if (labelLayout == "simple_v" || labelLayout == "column_v" || labelLayout == "row_v") rotate90(base) else base
    }

    /** 地点ラベルビットマップを生成する（標本番号なし） */
    fun buildLocationLabelBitmap(record: CollectionRecord, tapeWidthMm: Int): Bitmap {
        if (labelLayout == "auto_v") return buildAutoPortrait(tapeWidthMm, record.locationId, record)
        if (labelLayout == "auto_h") return buildAutoLandscape(tapeWidthMm, record.locationId, record)
        val h = TAPE_HEIGHT_DOTS[tapeWidthMm] ?: 128
        val w = mmToDots(labelLengthMm).coerceAtLeast(mmToDots(30))
        val isV = labelLayout == "simple_v" || labelLayout == "column_v"
        val base = when (labelLayout) {
            "column_h", "column_v" -> buildColumnH(w, h, record.locationId, record, isVertical = isV)
            "row_h",    "row_v"    -> buildRowH   (w, h, record.locationId, record)
            else                   -> buildSimpleH(w, h, record.locationId, record, isVertical = isV)
        }
        return if (labelLayout == "simple_v" || labelLayout == "column_v" || labelLayout == "row_v") rotate90(base) else base
    }

    // ════════════════════════════════════════════════════════════════════
    // レイアウト実装
    // ════════════════════════════════════════════════════════════════════

    // ── テスト印刷 ────────────────────────────────────────────────────
    private fun buildTestBitmap(tapeWidthMm: Int): Bitmap {
        val h = TAPE_HEIGHT_DOTS[tapeWidthMm] ?: 128
        val w = mmToDots(labelLengthMm).coerceAtLeast(mmToDots(40))
        val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bmp); canvas.drawColor(Color.WHITE)
        val m = (h * 0.025f).toInt().coerceAtLeast(1)
        val base = h * 0.28f * labelFontScale
        var y = m.toFloat()
        val l1 = makeLayout("テスト印刷", base, w - 2 * m, bold = true)
        canvas.save(); canvas.translate(m.toFloat(), y); l1.draw(canvas); canvas.restore(); y += l1.height + m
        val l2 = makeLayout("CollectionLabel App", base * 0.7f, w - 2 * m)
        canvas.save(); canvas.translate(m.toFloat(), y); l2.draw(canvas); canvas.restore(); y += l2.height + m / 2
        val l3 = makeLayout(printerLocalName.ifBlank { "---" }, base * 0.55f, w - 2 * m)
        canvas.save(); canvas.translate(m.toFloat(), y); l3.draw(canvas); canvas.restore()
        drawBorder(canvas, w, h)
        return if (labelLayout.endsWith("_v") || labelLayout == "auto_v") rotate90(bmp) else bmp
    }

    // ── シンプル横 / シンプル縦（縦は事後 rotate90）────────────────────
    //
    //  ┌─────────────────────────────────────────┐
    //  │  LOC-2026-0001-01                  [QR] │
    //  │  2026-05-13                             │
    //  │  東京都千代田区  標高123m               │
    //  │  採集者: 田中                            │
    //  │  備考テキスト                            │
    //  └─────────────────────────────────────────┘
    private fun buildSimpleH(minW: Int, h: Int, idText: String, record: CollectionRecord, isVertical: Boolean = false): Bitmap {
        val m   = (h * 0.025f).toInt().coerceAtLeast(1)
        val gap = (h * 0.012f).toInt().coerceAtLeast(1)
        val qrSize = if (showQr) (h - 2 * m).coerceAtLeast(0) else 0
        val textH  = h - 2 * m

        val locationText = if (record.altitude != null)
            "${record.displayLocation}  標高${record.altitude.toInt()}m"
        else record.displayLocation

        // 可視要素リスト
        data class Elem(val text: String, val scale: Float, val bold: Boolean = false, val mono: Boolean = false)
        val elems = buildList {
            add(Elem(idText, elemIdScale, bold = true, mono = true))
            if (showDate)                               add(Elem(record.collectionDate.toString(), elemDateScale))
            if (showLocation)                           add(Elem(locationText,                    elemLocationScale))
            if (showCoordinates)                        add(Elem("${"%.5f".format(record.latitude)}°N  ${"%.5f".format(record.longitude)}°E", elemLocationScale, mono = true))
            if (showCollector)                          add(Elem("採集者: ${record.collectorName}", elemCollectorScale))
            if (showNotes && record.notes.isNotBlank()) add(Elem(record.notes,                    elemNotesScale))
        }

        val baseSize = h * 0.24f * labelFontScale

        if (elems.isEmpty()) {
            val bmp = Bitmap.createBitmap(minW.coerceAtLeast(1), h, Bitmap.Config.ARGB_8888)
            Canvas(bmp).also { it.drawColor(Color.WHITE); drawBorder(it, minW.coerceAtLeast(1), h) }
            return bmp
        }

        // ② 各要素の自然幅・単行高さを計測（折り返しなし）
        val MEASURE_W = 99999
        data class ElemMetrics(val naturalW: Int, val lineH: Int)
        val elemMetrics = elems.map { e ->
            val tp = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                textSize = (baseSize * e.scale).coerceAtLeast(4f)
                typeface = resolveTypeface(e.bold, e.mono)
            }
            ElemMetrics(
                naturalW = tp.measureText(e.text).toInt() + 2,
                lineH    = makeLayout(e.text, (baseSize * e.scale).coerceAtLeast(4f), MEASURE_W, e.bold, e.mono).height
            )
        }

        // ③ 貪欲法でカラム分割
        //    各カラムは高さ <= textH を保証。フォント縮小なし、列が溢れたら新カラムへ。
        //    カラムが増えるほどラベルはテープ送り方向（横幅）へ延長される。
        val colGap = gap * 3
        data class ColData(val elems: List<Elem>, val metrics: List<ElemMetrics>)
        val columns   = mutableListOf<ColData>()
        var colElems  = mutableListOf<Elem>()
        var colMets   = mutableListOf<ElemMetrics>()
        var colH      = 0

        elems.forEachIndexed { i, e ->
            val em   = elemMetrics[i]
            val addH = if (colElems.isEmpty()) em.lineH else em.lineH + gap
            if (colElems.isNotEmpty() && colH + addH > textH) {
                columns.add(ColData(colElems.toList(), colMets.toList()))
                colElems = mutableListOf(e); colMets = mutableListOf(em); colH = em.lineH
            } else {
                colElems.add(e); colMets.add(em); colH += addH
            }
        }
        if (colElems.isNotEmpty()) columns.add(ColData(colElems.toList(), colMets.toList()))

        // ④ 各カラム幅 = そのカラム内の最大自然幅（折り返しなし）
        val colWidths    = columns.map { col -> col.metrics.maxOf { it.naturalW } }
        val totalTextW   = colWidths.sum() + colGap * (columns.size - 1)
        val neededTotalW = 2 * m + totalTextW + (if (qrSize > 0) gap + qrSize else 0)
        val actualW      = maxOf(minW, neededTotalW)

        val bmp    = Bitmap.createBitmap(actualW, h, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bmp); canvas.drawColor(Color.WHITE)

        var xOff = m
        columns.forEachIndexed { ci, col ->
            val colW = colWidths[ci]
            var y = m.toFloat()
            col.elems.forEachIndexed { ei, e ->
                val layout = makeLayout(e.text, (baseSize * e.scale).coerceAtLeast(4f), colW.coerceAtLeast(1), e.bold, e.mono)
                canvas.save(); canvas.translate(xOff.toFloat(), y); layout.draw(canvas); canvas.restore()
                y += layout.height + gap
            }
            xOff += colW + colGap
        }

        if (qrSize > 0) drawQr(canvas, record.qrPayload, actualW - m - qrSize, m, qrSize)
        drawBorder(canvas, actualW, h)
        return bmp
    }

    // ── セクション横 / セクション縦（縦は事後 rotate90）────────────────
    //
    //  ┌──────────┬────────┬───────────┬────────┬──────┐
    //  │ ID       │ 日付   │ 地点      │ 採集者 │ [QR] │
    //  │LOC-2026  │2026    │東京都     │田中    │      │
    //  │-0001-01  │-05-13  │千代田区   │        │      │
    //  └──────────┴────────┴───────────┴────────┴──────┘
    // ── セクション横 / セクション縦（縦は事後 rotate90）────────────────
    //
    //  各セクション幅をテキストの「2行折り返し幅」から自動算出する。
    //  重みによる固定配分をやめ、文字列の長さに正確に合わせることで
    //  余白の無駄をなくす。値テキストはセクション内で中央揃え。
    //
    //  ┌──────────┬────────┬───────────┬────────┬──────┐
    //  │    ID    │  日付  │   地点    │ 採集者 │ [QR] │
    //  │ LOC-2026 │  2026  │  東京都   │  田中  │      │
    //  │ -0001-01 │ -05-13 │ 千代田区  │        │      │
    //  └──────────┴────────┴───────────┴────────┴──────┘
    private fun buildColumnH(minW: Int, h: Int, idText: String, record: CollectionRecord, isVertical: Boolean = false): Bitmap {
        val m   = (h * 0.02f).toInt().coerceAtLeast(1)   // 最小余白
        val gap = (h * 0.012f).toInt().coerceAtLeast(1)

        val locationTextC = if (record.altitude != null)
            "${record.displayLocation}  標高${record.altitude.toInt()}m"
        else record.displayLocation

        data class Sec(val header: String, val value: String, val scale: Float, val weight: Float,
                       val bold: Boolean = false, val mono: Boolean = false)
        val sections = buildList {
            add(Sec("ID", idText, elemIdScale, elemIdWeight, bold = true, mono = true))
            if (showDate)                               add(Sec("日付",   record.collectionDate.toString(), elemDateScale,      elemDateWeight))
            if (showLocation)                           add(Sec("地点",   locationTextC,                    elemLocationScale,  elemLocationWeight))
            if (showCoordinates)                        add(Sec("座標",   "${"%.5f".format(record.latitude)}°N  ${"%.5f".format(record.longitude)}°E", elemLocationScale, elemLocationWeight, mono = true))
            if (showCollector)                          add(Sec("採集者", record.collectorName,              elemCollectorScale, elemCollectorWeight))
            if (showNotes && record.notes.isNotBlank()) add(Sec("備考",   record.notes,                     elemNotesScale,     elemNotesWeight))
        }

        val qrW        = if (showQr) (h - 2 * m).coerceAtLeast(0) else 0
        val baseSize   = h * 0.24f * labelFontScale
        val headerSize = (h * 0.15f * labelFontScale).coerceAtLeast(4f)

        val bmp    = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
        if (sections.isEmpty()) { return bmp }

        // ── 各セクションの最適テキスト幅を算出 ─────────────────────────────
        // 「値テキストが valueH（テープ内利用可能縦幅）に収まる最小幅」を二分探索で求める。
        // フォント縮小は行わず、必要に応じてラベルをテープ送り方向（横幅）へ延長する。

        // テープ高さから使用可能な値テキスト縦幅を事前算出
        val approxHeaderH  = makeLayout("X", headerSize, 99999).height
        val approxValueH   = (h - 2 * m - approxHeaderH - gap).coerceAtLeast(4)

        fun minWidthForHeight(
            text: String, textSize: Float, bold: Boolean, mono: Boolean, maxH: Int
        ): Int {
            val ts = textSize.coerceAtLeast(4f)
            val tp = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                this.textSize = ts; typeface = resolveTypeface(bold, mono)
            }
            val naturalW = tp.measureText(text).toInt().coerceAtLeast(1)
            // 自然幅（1行）でも収まらない場合はそのまま返す（フォント縮小なし）
            if (makeLayout(text, ts, naturalW, bold, mono).height > maxH) return naturalW
            // 二分探索: height <= maxH を満たす最小幅
            var lo = 1; var hi = naturalW
            while (lo < hi - 1) {
                val mid = (lo + hi) / 2
                if (makeLayout(text, ts, mid, bold, mono).height <= maxH) hi = mid else lo = mid
            }
            return hi
        }

        // 各セクション: 値テキストが approxValueH に収まる最小幅 と ヘッダ幅の大きい方
        val sectionTextWidths = sections.map { sec ->
            val valSz  = (baseSize * sec.scale).coerceAtLeast(4f)
            val valW   = minWidthForHeight(sec.value, valSz, sec.bold, sec.mono, approxValueH)
            val tpH    = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                textSize = headerSize; typeface = resolveTypeface(false, false)
            }
            val headW  = tpH.measureText(sec.header).toInt()
            maxOf(valW, headW)
        }
        val sectionWidths = sectionTextWidths.map { it + 2 * m }

        // 合計テキスト幅を計算し、minW を下回る場合は比例拡張
        val textAreaW   = sectionWidths.sum()
        val computedW   = textAreaW + qrW
        val actualW     = maxOf(minW, computedW)
        val scaleFactor = if (textAreaW > 0 && computedW < minW)
            (actualW - qrW).toFloat() / textAreaW else 1f

        val finalBmp = Bitmap.createBitmap(actualW, h, Bitmap.Config.ARGB_8888)
        val canvas   = Canvas(finalBmp); canvas.drawColor(Color.WHITE)
        val divPaint = Paint().apply { color = Color.BLACK; strokeWidth = 1.5f }

        // ── セクション描画（値テキストを中央揃え）────────────────────────
        var x = 0
        sections.forEachIndexed { idx, sec ->
            val secW   = (sectionWidths[idx] * scaleFactor).toInt().coerceAtLeast(4)
            val innerW = (secW - 2 * m).coerceAtLeast(1)
            val valSz  = (baseSize * sec.scale).coerceAtLeast(4f)

            // ヘッダ：中央揃え（フォント縮小なし）
            val hLayout = makeLayout(sec.header, headerSize, innerW,
                                     align = Layout.Alignment.ALIGN_CENTER)
            // 値：中央揃え（フォント縮小なし。幅は approxValueH に基づいて計算済み）
            val vLayout = makeLayout(sec.value, valSz, innerW,
                                     sec.bold, sec.mono, Layout.Alignment.ALIGN_CENTER)

            canvas.save(); canvas.translate((x + m).toFloat(), m.toFloat())
            hLayout.draw(canvas); canvas.restore()

            canvas.save(); canvas.translate((x + m).toFloat(), (m + hLayout.height + gap).toFloat())
            vLayout.draw(canvas); canvas.restore()

            // セクション区切り線（全高）
            if (idx < sections.size - 1) {
                canvas.drawLine((x + secW).toFloat(), 0f, (x + secW).toFloat(), h.toFloat(), divPaint)
            }
            x += secW
        }

        // QR セクション
        if (qrW > 0) {
            canvas.drawLine((actualW - qrW).toFloat(), 0f, (actualW - qrW).toFloat(), h.toFloat(), divPaint)
            drawQr(canvas, record.qrPayload, actualW - qrW + m, m, qrW - 2 * m)
        }

        return finalBmp
    }

    // ── 縦型オート (auto_v) ─────────────────────────────────────────────
    //
    //  テープ幅 = ラベルの横幅（固定）
    //  ラベル長さ = 内容の合計高さから自動計算（可変）
    //
    //  ① portrait ビットマップ (tapeDots × autoLength) を生成
    //       ┌──────────────────────┐ ←tape_width_dots
    //       │ LOC-2026-0001-01     │
    //       │ 2026-05-13           │
    //       │ 東京都千代田区       │
    //       │ 採集者: 田中         │
    //       │ 備考テキスト         │
    //       │ ┌──────────────────┐ │
    //       │ │       QR         │ │
    //       │ └──────────────────┘ │
    //       └──────────────────────┘
    //  ② rotate90 して (autoLength × tapeDots) のプリンター用ビットマップに変換
    /**
     * @param doRotate  true = 90° 回転して横型ビットマップを返す（auto_v 用）
     *                  false = portrait のまま返す（Y 軸が送り方向）
     */
    private fun buildAutoPortrait(
        tapeWidthMm: Int, idText: String, record: CollectionRecord,
        doRotate: Boolean = true
    ): Bitmap {
        val tapeDots = TAPE_HEIGHT_DOTS[tapeWidthMm] ?: 128
        val m        = (tapeDots * 0.025f).toInt().coerceAtLeast(1)
        val gap      = (tapeDots * 0.015f).toInt().coerceAtLeast(1)
        val contentW = (tapeDots - 2 * m).coerceAtLeast(1)

        // ベースサイズはテープ幅基準（横幅いっぱいに文字を配置）
        val baseSize = contentW * 0.24f * labelFontScale

        val locationTextAV = if (record.altitude != null)
            "${record.displayLocation}  標高${record.altitude.toInt()}m"
        else record.displayLocation

        data class Elem(val text: String, val scale: Float, val bold: Boolean = false, val mono: Boolean = false)
        val elems = buildList {
            add(Elem(idText, elemIdScale, bold = true, mono = true))
            if (showDate)                               add(Elem(record.collectionDate.toString(), elemDateScale))
            if (showLocation)                           add(Elem(locationTextAV,                  elemLocationScale))
            if (showCoordinates)                        add(Elem("${"%.5f".format(record.latitude)}°N  ${"%.5f".format(record.longitude)}°E", elemLocationScale, mono = true))
            if (showCollector)                          add(Elem("採集者: ${record.collectorName}", elemCollectorScale))
            if (showNotes && record.notes.isNotBlank()) add(Elem(record.notes,                    elemNotesScale))
        }

        // StaticLayout で各要素の自然な高さを計算（折り返しあり）
        val layouts = elems.map { e ->
            makeLayout(e.text, (baseSize * e.scale).coerceAtLeast(4f), contentW, e.bold, e.mono)
        }

        val qrSize = if (showQr) contentW else 0   // QR はコンテンツ幅いっぱい

        // ラベル長さ = マージン + 全要素の高さ + ギャップ + QR（オプション）+ マージン
        val autoLength = m +
            layouts.sumOf { it.height + gap } +
            (if (qrSize > 0) qrSize + gap else 0) +
            m

        // portrait ビットマップを生成（テープ幅 × 自動長さ）
        val bmpPortrait = Bitmap.createBitmap(tapeDots, autoLength, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bmpPortrait)
        canvas.drawColor(Color.WHITE)

        var y = m.toFloat()
        layouts.forEach { layout ->
            canvas.save(); canvas.translate(m.toFloat(), y); layout.draw(canvas); canvas.restore()
            y += layout.height + gap
        }

        if (qrSize > 0) {
            drawQr(canvas, record.qrPayload, m, y.toInt(), qrSize)
        }

        drawBorder(canvas, tapeDots, autoLength)

        // doRotate=true (auto_v): 90° 回転してプリンター送信フォーマットに変換
        //   portrait (tapeDots × autoLength) → landscape (autoLength × tapeDots)
        // doRotate=false (auto_h): portrait のまま送る
        //   SDK がビットマップ高さ方向を送り方向として解釈するため、
        //   Y 軸（高さ）が送り方向となり要素が送り方向に積み重なる
        return if (doRotate) rotate90(bmpPortrait) else bmpPortrait
    }

    // ── 横型オート (auto_h) ─────────────────────────────────────────────
    //
    //  buildSimpleH と同様のランドスケープビットマップ（幅広 X）を使用し、
    //  高品質なテキスト描画を維持する。
    //
    //  ビットマップ幅  (X) = 左右マージン + テキスト自然幅 + gap + QR幅（固定）
    //  ビットマップ高さ (Y) = 送り方向。要素が増えて溢れると自動延長。
    //
    //  レイアウト:
    //    左  = 全テキスト要素を Y 方向に積み重ね（カラム分割なし）
    //    右  = QR コード（h - 2*m の正方形、Y=m 先頭固定）
    //
    //  要素が増えるほど Y（送り方向）が延長され、フォントサイズは変化しない。
    private fun buildAutoLandscape(tapeWidthMm: Int, idText: String, record: CollectionRecord): Bitmap {
        val h       = TAPE_HEIGHT_DOTS[tapeWidthMm] ?: 128   // テープ幅ドット数（Y/QRサイズ基準）
        val m       = (h * 0.025f).toInt().coerceAtLeast(1)
        val gap     = (h * 0.012f).toInt().coerceAtLeast(1)
        val qrSize  = if (showQr) (h - 2 * m).coerceAtLeast(0) else 0
        val baseSize = h * 0.24f * labelFontScale

        val locationText = if (record.altitude != null)
            "${record.displayLocation}  標高${record.altitude.toInt()}m"
        else record.displayLocation

        data class Elem(val text: String, val scale: Float, val bold: Boolean = false, val mono: Boolean = false)
        val elems = buildList {
            add(Elem(idText, elemIdScale, bold = true, mono = true))
            if (showDate)                               add(Elem(record.collectionDate.toString(), elemDateScale))
            if (showLocation)                           add(Elem(locationText,                    elemLocationScale))
            if (showCoordinates)                        add(Elem("${"%.5f".format(record.latitude)}°N  ${"%.5f".format(record.longitude)}°E", elemLocationScale, mono = true))
            if (showCollector)                          add(Elem("採集者: ${record.collectorName}", elemCollectorScale))
            if (showNotes && record.notes.isNotBlank()) add(Elem(record.notes,                    elemNotesScale))
        }

        if (elems.isEmpty()) {
            val bmp = Bitmap.createBitmap(h, h, Bitmap.Config.ARGB_8888)
            Canvas(bmp).also { it.drawColor(Color.WHITE); drawBorder(it, h, h) }
            return bmp
        }

        // 各要素の自然幅を計測（折り返しなし）
        val naturalWidths = elems.map { e ->
            val tp = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                textSize = (baseSize * e.scale).coerceAtLeast(4f)
                typeface = resolveTypeface(e.bold, e.mono)
            }
            tp.measureText(e.text).toInt() + 2
        }

        // テキスト列幅 = 全要素の最大自然幅
        val textW = (naturalWidths.maxOrNull() ?: (h - 2 * m)).coerceAtLeast(1)

        // ビットマップ幅（X）= 左右マージン + テキスト幅 + QR（ランドスケープ）
        val neededW = 2 * m + textW + (if (qrSize > 0) gap + qrSize else 0)
        val actualW = neededW.coerceAtLeast(h)

        // テキストレイアウト（textW 内で折り返し）
        val layouts = elems.map { e ->
            makeLayout(e.text, (baseSize * e.scale).coerceAtLeast(4f), textW, e.bold, e.mono)
        }

        // ビットマップ高さ（Y=送り方向）: テープ高さ基準 vs テキスト合計の大きい方
        val totalTextH = m + layouts.sumOf { it.height + gap } + m
        val autoH = maxOf(h, totalTextH, if (qrSize > 0) 2 * m + qrSize else 0).coerceAtLeast(1)

        val bmp    = Bitmap.createBitmap(actualW, autoH, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bmp)
        canvas.drawColor(Color.WHITE)

        // テキストを Y（送り方向）に積み重ねる
        var y = m.toFloat()
        layouts.forEach { layout ->
            canvas.save(); canvas.translate(m.toFloat(), y); layout.draw(canvas); canvas.restore()
            y += layout.height + gap
        }

        // QR を右側に配置（曲率補正なし）
        if (qrSize > 0) {
            drawQr(canvas, record.qrPayload, actualW - m - qrSize, m, qrSize)
        }

        drawBorder(canvas, actualW, autoH)
        return bmp
    }

    // ════════════════════════════════════════════════════════════════════
    // 共通ヘルパー
    // ════════════════════════════════════════════════════════════════════

    /**
     * フォントファミリー設定・全体ボールド設定を考慮した Typeface を返す。
     *
     * - [bold]         : 要素固有のボールド指定（ID フィールドなど）
     * - [labelFontBold]: 設定画面で有効にした「全体ボールド」— いずれかが true なら BOLD を適用
     * - [labelFontFamily] が設定されている場合はそのファミリーを使用する
     */
    private fun resolveTypeface(bold: Boolean, mono: Boolean): Typeface {
        val effectiveBold = bold || labelFontBold
        val style = if (effectiveBold) Typeface.BOLD else Typeface.NORMAL
        return if (labelFontFamily.isNotBlank()) {
            try { Typeface.create(labelFontFamily, style) }
            catch (_: Throwable) { if (effectiveBold) Typeface.DEFAULT_BOLD else Typeface.DEFAULT }
        } else {
            when {
                mono && effectiveBold -> Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
                mono                  -> Typeface.MONOSPACE
                effectiveBold         -> Typeface.DEFAULT_BOLD
                else                  -> Typeface.DEFAULT
            }
        }
    }

    /**
     * StaticLayout を生成する。
     * テキストは maxWidth 内で自動折り返しされ、はみ出しが発生しない。
     * @param align 文字揃え（デフォルト左揃え、ALIGN_CENTER で中央揃え）
     */
    private fun makeLayout(
        text:     String,
        textSize: Float,
        maxWidth: Int,
        bold:     Boolean = false,
        mono:     Boolean = false,
        align:    Layout.Alignment = Layout.Alignment.ALIGN_NORMAL
    ): StaticLayout {
        val paint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
            color         = Color.BLACK
            this.textSize = textSize.coerceAtLeast(1f)
            typeface      = resolveTypeface(bold, mono)
        }
        return StaticLayout.Builder
            .obtain(text, 0, text.length, paint, maxWidth.coerceAtLeast(1))
            .setAlignment(align)
            .setLineSpacing(0f, 1.15f)
            .setIncludePad(false)
            .build()
    }

    // ── 縦セクション横 / 縦セクション縦（縦は事後 rotate90）──────────────
    //
    //  ┌────────┬─────────────────────────┐
    //  │ ID     │ LOC-2026-0001-01        │ ← 重み elemIdWeight
    //  ├────────┼─────────────────────────┤
    //  │ 日付   │ 2026-05-13              │ ← 重み elemDateWeight
    //  ├────────┼─────────────────────────┤
    //  │ 地点   │ 東京都千代田区          │ ← 重み elemLocationWeight
    //  ├────────┼─────────────────────────┤
    //  │ 採集者 │ 田中                    │ ← 重み elemCollectorWeight
    //  └────────┴─────────────────────────┘
    //  左列（ヘッダ）= 全幅の約 22%  右列（値）= 残り
    //  各行の高さは占有比（weight）で按分
    private fun buildRowH(minW: Int, h: Int, idText: String, record: CollectionRecord): Bitmap {
        val m = (h * 0.02f).toInt().coerceAtLeast(1)

        val locationTextR = if (record.altitude != null)
            "${record.displayLocation}  標高${record.altitude.toInt()}m"
        else record.displayLocation

        data class Sec(val header: String, val value: String, val scale: Float, val weight: Float,
                       val bold: Boolean = false, val mono: Boolean = false)
        val sections = buildList {
            add(Sec("ID",   idText,                               elemIdScale,        elemIdWeight,        bold = true, mono = true))
            if (showDate)                               add(Sec("日付",   record.collectionDate.toString(), elemDateScale,      elemDateWeight))
            if (showLocation)                           add(Sec("地点",   locationTextR,                    elemLocationScale,  elemLocationWeight))
            if (showCoordinates)                        add(Sec("座標",   "${"%.5f".format(record.latitude)}°N  ${"%.5f".format(record.longitude)}°E", elemLocationScale, elemLocationWeight, mono = true))
            if (showCollector)                          add(Sec("採集者", record.collectorName,              elemCollectorScale, elemCollectorWeight))
            if (showNotes && record.notes.isNotBlank()) add(Sec("備考",   record.notes,                     elemNotesScale,     elemNotesWeight))
        }

        if (sections.isEmpty()) {
            val bmp = Bitmap.createBitmap(minW.coerceAtLeast(1), h, Bitmap.Config.ARGB_8888)
            Canvas(bmp).also { it.drawColor(Color.WHITE); drawBorder(it, minW, h) }
            return bmp
        }

        val totalWgt   = sections.sumOf { it.weight.toDouble() }.toFloat().coerceAtLeast(0.001f)
        val innerH     = (h - 2 * m).coerceAtLeast(1)
        val headerFrac = 0.22f     // ヘッダ列の幅比率
        val baseSize   = (innerH.toFloat() / sections.size.coerceAtLeast(1)) * 0.60f * labelFontScale

        // 各行の値テキストが 1 行表示できる最小幅から actualW を算出
        val neededW = sections.maxOfOrNull { sec ->
            val tp = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
                textSize = (baseSize * sec.scale).coerceAtLeast(4f)
                typeface  = resolveTypeface(sec.bold, sec.mono)
            }
            val valNeedW = tp.measureText(sec.value).toInt() + 2 + m * 2
            (valNeedW / (1f - headerFrac)).toInt()
        } ?: 0

        val actualW  = maxOf(minW, neededW).coerceAtLeast(1)
        val headerW  = (actualW * headerFrac).toInt().coerceAtLeast(m * 2)
        val headerIW = (headerW - 2 * m).coerceAtLeast(1)
        val valueIW  = (actualW - headerW - 2 * m).coerceAtLeast(1)

        val bmp    = Bitmap.createBitmap(actualW, h, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bmp); canvas.drawColor(Color.WHITE)
        val divPaint = Paint().apply { color = Color.BLACK; strokeWidth = 1.5f }

        var y = m
        sections.forEachIndexed { idx, sec ->
            val rowH      = ((innerH * sec.weight / totalWgt).toInt()).coerceAtLeast(4)
            val hm        = (m * 0.5f).toInt().coerceAtLeast(1)

            // ヘッダ描画（行内上下中央）
            val headerSz  = (rowH * 0.38f * labelFontScale).coerceAtLeast(4f)
            val hLayout   = makeLayout(sec.header, headerSz, headerIW)
            val hOffY     = ((rowH - hLayout.height) / 2).coerceAtLeast(0)
            canvas.save(); canvas.translate(hm.toFloat(), (y + hOffY).toFloat()); hLayout.draw(canvas); canvas.restore()

            // 値描画（行高さに合わせて fit-scale、上下中央）
            var vFit = 1.0f
            repeat(5) {
                val vl = makeLayout(sec.value, (baseSize * sec.scale * vFit).coerceAtLeast(4f), valueIW, sec.bold, sec.mono)
                val maxVH = rowH - 2 * hm
                if (vl.height > maxVH && vl.height > 0) vFit *= maxVH.toFloat() / vl.height
            }
            val vLayout = makeLayout(sec.value, (baseSize * sec.scale * vFit).coerceAtLeast(4f), valueIW, sec.bold, sec.mono)
            val vOffY   = ((rowH - vLayout.height) / 2).coerceAtLeast(0)
            canvas.save(); canvas.translate((headerW + m).toFloat(), (y + vOffY).toFloat()); vLayout.draw(canvas); canvas.restore()

            // 行区切り線
            if (idx < sections.size - 1) {
                val lineY = (y + rowH).toFloat()
                canvas.drawLine(0f, lineY, actualW.toFloat(), lineY, divPaint)
            }
            y += rowH
        }

        // ヘッダと値の縦区切り線
        canvas.drawLine(headerW.toFloat(), m.toFloat(), headerW.toFloat(), (h - m).toFloat(), divPaint)

        drawBorder(canvas, actualW, h)
        return bmp
    }

    /**
     * ビットマップ群を水平方向（幅方向＝繰り出し方向）に連結する。
     * 横型ランドスケープレイアウト用。
     * @param gapDots ラベル間の白紙ギャップ（ドット数）
     */
    private fun concatenateBitmapsH(bitmaps: List<Bitmap>, gapDots: Int = 0): Bitmap {
        if (bitmaps.isEmpty()) return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
        if (bitmaps.size == 1) return bitmaps[0]
        val h         = bitmaps[0].height
        val totalW    = bitmaps.sumOf { it.width } + gapDots * (bitmaps.size - 1)
        val combined  = Bitmap.createBitmap(totalW, h, Bitmap.Config.ARGB_8888)
        val canvas    = Canvas(combined)
        canvas.drawColor(Color.WHITE)
        var x = 0
        bitmaps.forEachIndexed { idx, bmp ->
            canvas.drawBitmap(bmp, x.toFloat(), 0f, null)
            x += bmp.width
            if (idx < bitmaps.size - 1) x += gapDots
        }
        return combined
    }

    /**
     * ビットマップ群を垂直方向（高さ方向＝繰り出し方向）に連結する。
     * Brother PT SDK はビットマップの高さ方向を繰り出し方向として解釈するため、
     * 全レイアウトに共通して使用する。
     * auto_v 等で各ラベルの幅が異なる場合は最大幅のキャンバスに左揃えで配置する。
     * @param gapDots ラベル間の白紙ギャップ（ドット数）
     */
    private fun concatenateBitmapsV(bitmaps: List<Bitmap>, gapDots: Int = 0): Bitmap {
        if (bitmaps.isEmpty()) return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
        if (bitmaps.size == 1) return bitmaps[0]
        val w         = bitmaps.maxOf { it.width }
        val totalH    = bitmaps.sumOf { it.height } + gapDots * (bitmaps.size - 1)
        val combined  = Bitmap.createBitmap(w, totalH, Bitmap.Config.ARGB_8888)
        val canvas    = Canvas(combined)
        canvas.drawColor(Color.WHITE)
        var y = 0
        bitmaps.forEachIndexed { idx, bmp ->
            canvas.drawBitmap(bmp, 0f, y.toFloat(), null)
            y += bmp.height
            if (idx < bitmaps.size - 1) y += gapDots
        }
        return combined
    }

    /** 90° 時計回りに回転（縦向きラベル用） */
    private fun rotate90(src: Bitmap): Bitmap {
        val matrix = android.graphics.Matrix().apply { setRotate(90f) }
        return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
    }

    /** ZXing で QR コードを描画する。 */
    private fun drawQr(canvas: Canvas, url: String, left: Int, top: Int, size: Int) {
        if (size <= 0) return
        if (url.isBlank()) {
            android.util.Log.w("PrinterService", "drawQr: url is blank, skipping")
            return
        }
        try {
            val hints = mapOf(
                EncodeHintType.CHARACTER_SET    to "UTF-8",
                EncodeHintType.MARGIN           to 1,
                EncodeHintType.ERROR_CORRECTION to com.google.zxing.qrcode.decoder.ErrorCorrectionLevel.L
            )
            val matrix = QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, size, size, hints)
            val matW   = matrix.width
            val matH   = matrix.height

            val pixels = IntArray(matW * matH) { i ->
                if (matrix[i % matW, i / matW]) Color.BLACK else Color.WHITE
            }
            val qrBmp = Bitmap.createBitmap(pixels, matW, matH, Bitmap.Config.ARGB_8888)

            // ニアレストネイバー補間（isFilterBitmap=false）でシャープに拡大描画
            val paint = Paint().apply { isFilterBitmap = false }
            val dst   = android.graphics.RectF(
                left.toFloat(), top.toFloat(),
                (left + size).toFloat(), (top + size).toFloat()
            )
            canvas.drawBitmap(qrBmp, null, dst, paint)

        } catch (e: Throwable) {
            android.util.Log.e("PrinterService", "drawQr failed: ${e.javaClass.simpleName}: ${e.message}")
            canvas.drawRect(
                left.toFloat(), top.toFloat(), (left + size).toFloat(), (top + size).toFloat(),
                Paint().apply { color = Color.BLACK; style = Paint.Style.STROKE; strokeWidth = 1.5f }
            )
        }
    }

    /** 枠なし（ユーザー設定によりラベル枠を非表示） */
    @Suppress("UNUSED_PARAMETER")
    private fun drawBorder(canvas: Canvas, w: Int, h: Int) { /* no-op */ }
}

sealed class PrintResult {
    data class Success(val message: String) : PrintResult()
    data class Error(val message: String)   : PrintResult()
}
