#!/usr/bin/python3
#
# SPDX-License-Identifier: GPL-2.0-only
# (c) 2026 Gerd Hoffmann
#
""" build qemu command line """
import os
import json
import tempfile
import subprocess

# pylint: disable=too-many-public-methods
# pylint: disable=too-many-instance-attributes
class QemuCfg():
    """ build qemu command line """

    # pylint: disable=fixme
    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments
    def __init__(self, logger = None, pcie = True, scsi = True, nvme = False, sata = False):

        # config
        self.logger = logger
        self.use_pcie = pcie
        self.use_scsi = scsi
        self.use_nvme = nvme
        self.use_sata = sata

        # TODO: autodetect cbit
        self.sev_cbit = 51
        self.sev_kernel_hashes = False

        # state
        self.have_scsi = False
        self.have_xhci = False
        self.have_bot = False
        self.have_virtio_serial = False
        self.have_nvme = False

        # generate ids
        self.ids = {}
        self.pcie = 0

        # device / object lists
        self.blkdev = []
        self.netdev = []
        self.chardev = []
        self.devices = []
        self.objects = []
        self.machine = []


    def get_id(self, kind):
        self.ids.setdefault(kind, 0)
        retval = f'{kind}{self.ids[kind]}'
        self.ids[kind] += 1
        return retval

    @staticmethod
    def mkarg(value):
        if isinstance(value, dict):
            return json.dumps(value)
        return value

    def cmdline(self):
        cmdline = []
        for obj in self.objects:
            cmdline.append('-object')
            cmdline.append(self.mkarg(obj))
        for blk in self.blkdev:
            cmdline.append('-blockdev')
            cmdline.append(self.mkarg(blk))
        for net in self.netdev:
            cmdline.append('-netdev')
            cmdline.append(self.mkarg(net))
        for char in self.chardev:
            cmdline.append('-chardev')
            cmdline.append(self.mkarg(char))
        for dev in self.devices:
            cmdline.append('-device')
            cmdline.append(self.mkarg(dev))
        for m in self.machine:
            cmdline.append('-machine')
            cmdline.append(m)
        return cmdline

    # --- devices ---

    def add_pcie_port(self):
        name = f'port{self.pcie}'
        slot = 8 + (self.pcie >> 3)
        func = self.pcie & 7
        device = { 'driver'  : 'pcie-root-port',
                   'id'      : name,
                   'bus'     : 'pcie.0',
                   'addr'    : f'{slot:x}.{func:x}',
                   'chassis' : self.pcie }
        if func == 0:
            device['multifunction'] = True
        self.devices.append(device)
        self.pcie += 1
        return name

    def add_virtio_device(self, kind, args = None):
        devname = f'virtio-{kind}-pci'
        port = None
        if self.use_pcie:
            port = self.add_pcie_port()

        if args is None:
            args = {}
        if isinstance(args, dict):
            # json
            device = dict(args)
            device['driver'] = devname
            if port:
                device['bus'] = port
        else:
            # string
            device = f'{devname},{args}'
            if port:
                device += f',bus={port}'

        self.devices.append(device)

    def add_virtio_net(self, netdev, bootindex = None):
        args = { 'netdev' : netdev }
        if bootindex is not None:
            args['bootindex'] = bootindex
        self.add_virtio_device('net', args)

    def add_virtio_blk(self, blkdev, bootindex = None):
        args = { 'drive' : blkdev }
        if bootindex is not None:
            args['bootindex'] = bootindex
        self.add_virtio_device('blk', args)

    def add_virtio_scsi(self):
        self.add_virtio_device('scsi', { 'id' : 'virtio' })
        self.have_scsi = True

    def add_virtio_rng(self):
        self.add_virtio_device('rng')

    def add_virtio_serial(self):
        self.add_virtio_device('serial')
        self.have_virtio_serial = True

    def add_virtio_serial_port(self, chardev, name):
        if not self.have_virtio_serial:
            self.add_virtio_serial()
        device = { 'driver'  : 'virtserialport',
                   'chardev' : chardev,
                   'name'    : name }
        self.devices.append(device)

    def add_scsi_hd(self, blkdev, bootindex = None):
        if not self.have_scsi:
            self.add_virtio_scsi()
        device = { 'driver' : 'scsi-hd',
                   'bus'    : 'virtio.0',
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_scsi_cd(self, blkdev, bootindex = None):
        if not self.have_scsi:
            self.add_virtio_scsi()
        device = { 'driver' : 'scsi-cd',
                   'bus'    : 'virtio.0',
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_sata_hd(self, blkdev, bootindex = None):
        device = { 'driver' : 'ide-hd',
                   'bus'    : self.get_id('ide.'),
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_sata_cd(self, blkdev, bootindex = None):
        device = { 'driver' : 'ide-cd',
                   'bus'    : self.get_id('ide.'),
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_nvme_ctrl(self):
        device = { 'driver' : 'nvme',
                   'serial' : '1234' }
        if self.use_pcie:
            port = self.add_pcie_port()
            device['bus'] = port
        self.devices.append(device)
        self.have_nvme = True

    def add_nvme_ns(self, blkdev, bootindex = None):
        if not self.have_nvme:
            self.add_nvme_ctrl()
        device = { 'driver' : 'nvme-ns',
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_usb_xhci(self):
        ports = 8
        device = { 'driver' : 'qemu-xhci',
                   'p2'     : ports,
                   'p3'     : ports }
        if self.use_pcie:
            port = self.add_pcie_port()
            device['bus'] = port
        self.devices.append(device)
        self.have_xhci = True

    def add_usb_cd(self, blkdev, bootindex = None):
        if not self.have_xhci:
            self.add_usb_xhci()
        if not self.have_bot:
            self.devices.append({ 'driver' : 'usb-bot',
                                  'id'     : 'usb.bot' })
            self.have_bot = True
        device = { 'driver' : 'scsi-cd',
                   'bus'    : 'usb.bot.0',
                   'drive'  : blkdev }
        if bootindex is not None:
            device['bootindex'] = bootindex
        self.devices.append(device)

    def add_isa_debugcon(self, chardev, iobase = 0x402):
        device = { 'driver'  : 'isa-debugcon',
                   'chardev' : chardev,
                   'iobase'  : iobase }
        self.devices.append(device)

    # --- network ---

    def add_netdev_user(self, nid):
        netdev = { 'type' : 'user',
                   'id'   : nid }
        self.netdev.append(netdev)

    # --- storage ---

    def add_blkdev_qcow2(self, bid, filename):
        if self.logger:
            self.logger.info('# blkdev/qcow2: id=%s file=%s', bid, filename)
        blkdev = { 'node-name' : bid,
                   'driver'    : 'qcow2',
                   'file'      : { 'driver'   : 'file',
                                   'filename' : filename }}
        self.blkdev.append(blkdev)

    def add_blkdev_iso(self, bid, filename):
        if self.logger:
            self.logger.info('# blkdev/iso: id=%s file=%s', bid, filename)
        blkdev = { 'node-name' : bid,
                   'driver'    : 'file',
                   'filename'  : filename,
                   'read-only' : True }
        self.blkdev.append(blkdev)

    # --- chardev ---

    def add_chardev_logfile(self, cid, filename):
        chardev = f'file,id={cid},path={filename}'
        self.chardev.append(chardev)

    # --- helpers ---

    # pylint: disable=consider-using-with
    def create_tempfile_qcow2(self, image):
        copy = tempfile.NamedTemporaryFile(suffix = '.qcow2')
        if self.logger:
            self.logger.info('# qcow2: copy=%s backing=%s', copy.name, image)
        qemuimg = [ 'qemu-img', 'create', '-q', '-f', 'qcow2', '-F', 'qcow2',
                    '-b', os.path.abspath(image), copy.name ]
        subprocess.run(qemuimg, check = True)
        return copy

    def create_empty_qcow2(self, filename, size = '8G'):
        if os.path.exists(filename):
            if self.logger:
                self.logger.info("# qcow2: skip=%s [file exists]", filename)
            return

        if self.logger:
            self.logger.info("# qcow2: create=%s size=%s", filename, size)
        qemuimg = [ 'qemu-img', 'create', '-q', '-f', 'qcow2', filename, size ]
        subprocess.run(qemuimg, check = True)

    def add_disk_qcow2(self, filename, bootindex = None):
        bid = self.get_id('blk')
        self.add_blkdev_qcow2(bid, filename)
        if self.use_nvme:
            self.add_nvme_ns(blkdev = bid, bootindex = bootindex)
        elif self.use_scsi:
            self.add_scsi_hd(blkdev = bid, bootindex = bootindex)
        elif self.use_sata:
            self.add_sata_hd(blkdev = bid, bootindex = bootindex)
        else:
            self.add_virtio_blk(blkdev = bid, bootindex = bootindex)

    def add_cdrom_iso(self, filename, bootindex = None):
        bid = self.get_id('blk')
        self.add_blkdev_iso(bid, filename)
        if self.use_scsi:
            self.add_scsi_cd(blkdev = bid, bootindex = bootindex)
        elif self.use_sata:
            self.add_sata_cd(blkdev = bid, bootindex = bootindex)
        else:
            self.add_usb_cd(blkdev = bid, bootindex = bootindex)

    def add_network(self, bootindex = None):
        nid = self.get_id('net')
        self.add_netdev_user(nid = nid)
        self.add_virtio_net(netdev = nid, bootindex = bootindex)

    def add_virtio_serial_log(self, name, filename):
        cid = self.get_id('char')
        self.add_chardev_logfile(cid, filename)
        self.add_virtio_serial_port(cid, name)

    def add_x64_firmware_log(self, filename):
        cid = self.get_id('char')
        self.add_chardev_logfile(cid, filename)
        self.add_isa_debugcon(cid)

    # --- confidential ---

    def set_sev_cbit(self, cbit):
        self.sev_cbit = cbit

    def set_sev_kernel_hashes(self, kernel_hashes):
        self.sev_kernel_hashes = kernel_hashes

    def add_confidential_tdx(self):
        oid = self.get_id('obj')
        tdx = { 'qom-type' : 'tdx-guest',
                'id'       : oid }
        if os.path.exists('/run/tdx-qgs/qgs.socket'):
            qgs = { 'type' : 'unix',
                    'path' : '/run/tdx-qgs/qgs.socket' }
            tdx['quote-generation-socket'] = qgs
        self.objects.append(tdx)
        self.machine.append(f'confidential-guest-support={oid}')

    def add_confidential_sev_common(self, sev):
        oid = self.get_id('obj')
        sev['id']       = oid
        sev['cbitpos']  = self.sev_cbit
        sev['reduced-phys-bits'] = 1
        sev['kernel-hashes'] = self.sev_kernel_hashes
        self.objects.append(sev)
        self.machine.append(f'confidential-guest-support={oid}')

    def add_confidential_sev(self):
        sev = { 'qom-type' : 'sev-guest' }
        self.add_confidential_sev_common(sev)

    def add_confidential_sev_es(self):
        sev = { 'qom-type' : 'sev-guest',
                'policy'   : 5}
        self.add_confidential_sev_common(sev)

    def add_confidential_snp(self):
        sev = {'qom-type' : 'sev-snp-guest'}
        self.add_confidential_sev_common(sev)

    def add_confidential(self, cc_mode):
        if cc_mode == 'sev':
            self.add_confidential_sev()
        elif cc_mode in [ 'sev-es', 'es' ]:
            self.add_confidential_sev_es()
        elif cc_mode in [ 'sev-snp', 'snp' ]:
            self.add_confidential_snp()
        elif cc_mode == 'tdx':
            self.add_confidential_tdx()
