#!/usr/bin/python3
# Copyright (C) 2025  FreeIPA Contributors see COPYING for license

"""
pki - PKI client command (Dogtag-compatible)

Main CLI for interacting with ipacta CA/KRA, compatible with
Dogtag's pki command.
"""

import sys
import argparse
import os
import socket
import subprocess
import tempfile

try:
    from pki.client import PKIClient
    from pki.ca import CACertClient, CAClient
    from pki.profile import ProfileClient
    from pki.authority import AuthorityClient, AuthorityData
    from pki.system import SecurityDomainClient
    from pki.key import KeyClient
except ImportError:
    print("Error: pki module not found. Install ipacta package.",
          file=sys.stderr)
    sys.exit(1)


class PKICLIClient:
    """Main PKI CLI client"""

    def __init__(self):
        self.verbose = False
        self.debug = False
        self.connection = None
        self.pki_client = None
        self.ca_client = None
        self._temp_files = []

    def __del__(self):
        for f in self._temp_files:
            try:
                os.unlink(f)
            except OSError:
                pass

    def parse_global_args(self, args):
        """Parse global arguments"""
        parser = argparse.ArgumentParser(
            description='PKI client command',
            add_help=False,
            usage='pki [OPTIONS] <command> [command-options]'
        )

        # Connection options
        parser.add_argument('-d', '--nssdb', metavar='PATH',
                            help='NSS database location')
        parser.add_argument('-c', '--password', metavar='PASSWORD',
                            help='NSS database password')
        parser.add_argument('-C', '--password-file', metavar='FILE',
                            help='NSS database password file')
        parser.add_argument('-n', '--nickname', metavar='NICKNAME',
                            help='Client certificate nickname')
        parser.add_argument('-e', '--cert-file', metavar='FILE',
                            help='PEM client certificate file')
        parser.add_argument('-k', '--key-file', metavar='FILE',
                            help='PEM client key file')
        parser.add_argument('-u', '--username', metavar='USERNAME',
                            help='Username for basic authentication')
        parser.add_argument('-w', '--user-password', metavar='PASSWORD',
                            help='Password for basic authentication')
        parser.add_argument('-W', '--user-password-file', metavar='FILE',
                            help='Password file for basic authentication')

        # Server connection
        parser.add_argument('-U', '--url', metavar='URL',
                            help='PKI server URL')
        parser.add_argument('-P', '--protocol', default='https',
                            help='Protocol (default: https)')
        parser.add_argument('-h', '--hostname', default=socket.getfqdn(),
                            help='Hostname (default: FQDN)')
        parser.add_argument('-p', '--port', default='8443',
                            help='Port (default: 8443)')
        parser.add_argument('-t', '--subsystem', default='ca',
                            help='Subsystem type (default: ca)')

        # Token / password.conf options
        parser.add_argument('-f', '--password-conf', metavar='FILE',
                            help='Password config file (password.conf)')
        parser.add_argument('--token', metavar='TOKEN',
                            help='Security token name')

        # Output options
        parser.add_argument('-v', '--verbose', action='store_true',
                            help='Run in verbose mode')
        parser.add_argument('--debug', action='store_true',
                            help='Run in debug mode')
        parser.add_argument('--help', action='store_true',
                            help='Show help message')
        parser.add_argument('--version', action='store_true',
                            help='Show version')

        return parser.parse_known_args(args)

    # Options that take a following value argument
    _GLOBAL_VALUE_OPTS = {
        '-d', '--nssdb', '-c', '--password', '-C', '--password-file',
        '-n', '--nickname', '-e', '--cert-file', '-k', '--key-file',
        '-u', '--username', '-w', '--user-password',
        '-W', '--user-password-file', '-U', '--url', '-P', '--protocol',
        '-h', '--hostname', '-p', '--port', '-t', '--subsystem',
        '-f', '--password-conf', '--token',
    }

    def _split_at_command(self, args):
        """Split args into (global_args, command_and_rest).

        Finds the first positional argument (the subcommand name) and
        splits there, so that subcommand options like --password-file
        don't collide with global options of the same name.
        """
        i = 0
        while i < len(args):
            arg = args[i]
            if arg.startswith('-'):
                if arg in self._GLOBAL_VALUE_OPTS:
                    i += 2
                else:
                    i += 1
            else:
                return args[:i], args[i:]
        return args, []

    def _extract_nss_cert(self, nssdb, nickname, password):
        """Extract client cert and key from NSS database to temp PEM files."""
        p12_fd, p12_path = tempfile.mkstemp(suffix='.p12')
        os.close(p12_fd)

        pwd_fd, pwd_path = tempfile.mkstemp(suffix='.txt')
        with os.fdopen(pwd_fd, 'w') as f:
            f.write(password or '')

        export_fd, export_path = tempfile.mkstemp(suffix='.txt')
        with os.fdopen(export_fd, 'w') as f:
            f.write('pki-export-tmp')

        cert_fd, cert_path = tempfile.mkstemp(suffix='.pem')
        os.close(cert_fd)

        key_fd, key_path = tempfile.mkstemp(suffix='.key')
        os.close(key_fd)

        try:
            subprocess.run([
                'pk12util', '-o', p12_path,
                '-d', nssdb, '-n', nickname,
                '-k', pwd_path, '-w', export_path
            ], check=True, capture_output=True)

            subprocess.run([
                'openssl', 'pkcs12',
                '-in', p12_path, '-out', cert_path,
                '-clcerts', '-nokeys',
                '-passin', 'pass:pki-export-tmp',
            ], check=True, capture_output=True)

            subprocess.run([
                'openssl', 'pkcs12',
                '-in', p12_path, '-out', key_path,
                '-nocerts', '-nodes',
                '-passin', 'pass:pki-export-tmp',
            ], check=True, capture_output=True)

            return cert_path, key_path

        except subprocess.CalledProcessError as e:
            for f in [cert_path, key_path]:
                if os.path.exists(f):
                    os.unlink(f)
            raise RuntimeError(
                f"Failed to extract certificate '{nickname}' "
                f"from {nssdb}: {e.stderr.decode().strip()}")
        finally:
            for f in [p12_path, pwd_path, export_path]:
                if os.path.exists(f):
                    os.unlink(f)

    def _read_password_conf(self, password_conf, token=None):
        """Read password from a password.conf file.

        Returns the password for the given token (hardware-<token>=...)
        or the internal password if no token is specified.
        """
        with open(password_conf) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                if '=' not in line:
                    continue
                key, value = line.split('=', 1)
                key = key.strip()
                value = value.strip()
                if token and key == f'hardware-{token}':
                    return value
                if not token and key == 'internal':
                    return value
        return None

    def _get_nssdb_password_file(self, global_args):
        """Get NSSDB password file path from global args.

        Handles -f (password.conf), -C (password file), and -c (password).
        Returns the path to a password file suitable for pk12util -k,
        or None if no password was provided.
        """
        if global_args.password_conf:
            token = getattr(global_args, 'token', None)
            password = self._read_password_conf(
                global_args.password_conf, token)
            if password is not None:
                fd, pwd_file = tempfile.mkstemp(suffix='.pwd')
                with os.fdopen(fd, 'w') as f:
                    f.write(password)
                self._temp_files.append(pwd_file)
                return pwd_file
            return None

        if global_args.password_file:
            return global_args.password_file

        if global_args.password:
            fd, pwd_file = tempfile.mkstemp(suffix='.pwd')
            with os.fdopen(fd, 'w') as f:
                f.write(global_args.password)
            self._temp_files.append(pwd_file)
            return pwd_file

        return None

    def create_connection(self, args):
        """Create PKI connection from arguments"""
        if args.url:
            url = args.url
        else:
            url = f"{args.protocol}://{args.hostname}:{args.port}"

        self.pki_client = PKIClient(url)
        self.connection = self.pki_client.connection
        self.ca_client = CAClient(self.pki_client)

        if args.cert_file and args.key_file:
            self.pki_client.set_client_auth(args.cert_file, args.key_file)

        elif args.nssdb and args.nickname:
            password = args.password
            if args.password_file:
                with open(args.password_file) as f:
                    password = f.read().strip()
            cert_path, key_path = self._extract_nss_cert(
                args.nssdb, args.nickname, password)
            self.pki_client.set_client_auth(cert_path, key_path)
            self._temp_files.extend([cert_path, key_path])

        elif args.username:
            password = args.user_password
            if args.user_password_file:
                with open(args.user_password_file) as f:
                    password = f.read().strip()
            if password:
                self.pki_client.authenticate(args.username, password)

        self.verbose = args.verbose
        self.debug = args.debug

    def cmd_info(self, args):
        """Display server info"""
        info = self.ca_client.get_info()
        print("  Server: ipacta")
        print(f"  Version: {info.version}")

    def cmd_ca_cert_find(self, args):
        """Find certificates"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-find')
        parser.add_argument('--minSerialNumber',
                            help='Minimum serial number')
        parser.add_argument('--maxSerialNumber',
                            help='Maximum serial number')
        parser.add_argument('--status', help='Certificate status')
        parser.add_argument('--name', help='Subject common name')
        parser.add_argument('--start', type=int, default=0)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)

        search_params = {}
        if opts.status:
            search_params['status'] = opts.status
        if opts.minSerialNumber:
            search_params['serial_from'] = opts.minSerialNumber
        if opts.maxSerialNumber:
            search_params['serial_to'] = opts.maxSerialNumber
        if opts.name:
            search_params['common_name'] = opts.name

        certs = cert_client.list_certs(
            max_results=opts.size,
            start=opts.start,
            size=opts.size,
            **search_params
        )

        print(f"  {len(certs)} certificate(s) found")
        for cert in certs:
            print(f"  Serial Number: {cert.serial_number}")
            print(f"    Subject DN: {cert.subject_dn}")
            print(f"    Status: {cert.status}")
            print()

    def cmd_ca_cert_show(self, args):
        """Show certificate details"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-show')
        parser.add_argument('serial_number', help='Certificate serial number')
        parser.add_argument('--pretty', action='store_true',
                            help='Pretty print')
        parser.add_argument('--output', metavar='FILE',
                            help='Output file')

        opts = parser.parse_args(args)

        serial = opts.serial_number
        if serial.startswith('0x') or serial.startswith('0X'):
            serial_num = int(serial, 16)
        else:
            serial_num = int(serial)

        cert_client = CACertClient(self.ca_client)
        cert = cert_client.get_cert(serial_num)

        print(f"  Certificate ID: {cert.serial_number}")
        print(f"  Subject DN: {cert.subject_dn}")
        print(f"  Issuer DN: {cert.issuer_dn}")
        print(f"  Status: {cert.status}")
        print(f"  Not Before: {cert.not_before}")
        print(f"  Not After: {cert.not_after}")

        if opts.output:
            with open(opts.output, 'w') as f:
                f.write(cert.encoded)
            print(f"\n  Certificate saved to {opts.output}")

    def cmd_ca_cert_revoke(self, args):
        """Revoke certificate"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-revoke')
        parser.add_argument('serial_number', help='Certificate serial number')
        parser.add_argument('--reason', default='Unspecified',
                            help='Revocation reason')
        parser.add_argument('--comments', help='Revocation comments')

        opts = parser.parse_args(args)

        serial = opts.serial_number
        if serial.startswith('0x') or serial.startswith('0X'):
            serial_num = int(serial, 16)
        else:
            serial_num = int(serial)

        cert_client = CACertClient(self.ca_client)
        cert_client.revoke_cert(
            serial_num, revocation_reason=opts.reason,
            comments=opts.comments)

        print(f"  Revoked certificate {serial}")

    def cmd_ca_cert_hold(self, args):
        """Place certificate on hold"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-hold')
        parser.add_argument('serial_number', help='Certificate serial number')
        parser.add_argument('--comments', help='Comments')

        opts = parser.parse_args(args)

        serial = opts.serial_number
        if serial.startswith('0x') or serial.startswith('0X'):
            serial_num = int(serial, 16)
        else:
            serial_num = int(serial)

        cert_client = CACertClient(self.ca_client)
        cert_client.hold_cert(serial_num, comments=opts.comments)

        print(f"  Placed certificate {serial} on hold")

    def cmd_ca_cert_release_hold(self, args):
        """Release certificate hold"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-release-hold')
        parser.add_argument('serial_number', help='Certificate serial number')

        opts = parser.parse_args(args)

        serial = opts.serial_number
        if serial.startswith('0x') or serial.startswith('0X'):
            serial_num = int(serial, 16)
        else:
            serial_num = int(serial)

        cert_client = CACertClient(self.ca_client)
        cert_client.unrevoke_cert(serial_num)

        print(f"  Released certificate {serial} from hold")

    def cmd_ca_cert_request_submit(self, args):
        """Submit certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-submit')
        parser.add_argument('--profile', required=True,
                            help='Certificate profile ID')
        parser.add_argument('--request', metavar='FILE',
                            help='Certificate request file (PKCS#10)')
        parser.add_argument('--request-type', default='pkcs10',
                            help='Request type (default: pkcs10)')

        opts = parser.parse_args(args)

        if opts.request:
            with open(opts.request, 'r') as f:
                csr_data = f.read()
        else:
            print("Error: --request required", file=sys.stderr)
            return 1

        from pki.cert import CertEnrollmentRequest
        cert_client = CACertClient(self.ca_client)

        request = CertEnrollmentRequest(profile_id=opts.profile)
        request.inputs = {
            'cert_request_type': opts.request_type,
            'cert_request': csr_data,
        }

        results = cert_client.enroll_cert(request)

        for result in results:
            print("  Submitted certificate request")
            print(f"  Request ID: {result.request.request_id}")
            if result.cert:
                print(f"  Certificate ID: {result.cert.serial_number}")
                print("  Status: COMPLETE")
            else:
                print(f"  Status: {result.request.request_status}")

        return 0

    def cmd_ca_cert_request_find(self, args):
        """Find certificate requests"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-find')
        parser.add_argument('--status',
                            help='Request status (pending, cancelled, '
                                 'rejected, complete)')
        parser.add_argument('--type', dest='request_type',
                            help='Request type (enrollment, renewal, '
                                 'revocation)')
        parser.add_argument('--start', type=int)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        requests = cert_client.list_requests(
            request_status=opts.status,
            request_type=opts.request_type,
            size=opts.size,
            max_results=opts.size
        )

        print(f"  {len(requests)} request(s) found")
        for req in requests:
            print(f"  Request ID: {req.request_id}")
            print(f"    Type: {req.request_type}")
            print(f"    Status: {req.request_status}")
            if req.cert_id:
                print(f"    Certificate ID: {req.cert_id}")
            print()

    def cmd_ca_cert_request_show(self, args):
        """Show certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-show')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        req = cert_client.get_request(opts.request_id)

        print(f"  Request ID: {req.request_id}")
        print(f"  Type: {req.request_type}")
        print(f"  Status: {req.request_status}")
        print(f"  Operation Result: {req.operation_result}")
        if req.cert_id:
            print(f"  Certificate ID: {req.cert_id}")
        if req.error_message:
            print(f"  Error: {req.error_message}")

    def cmd_ca_cert_request_approve(self, args):
        """Approve certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-approve')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.approve_request(opts.request_id)

        print(f"  Approved request {opts.request_id}")

    def cmd_ca_cert_request_reject(self, args):
        """Reject certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-reject')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.reject_request(opts.request_id)

        print(f"  Rejected request {opts.request_id}")

    def cmd_ca_cert_request_cancel(self, args):
        """Cancel certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-cancel')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.cancel_request(opts.request_id)

        print(f"  Cancelled request {opts.request_id}")

    def cmd_ca_profile_find(self, args):
        """List certificate profiles"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-find')
        parser.add_argument('--start', type=int, default=0)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)
        profiles = profile_client.list_profiles(
            start=opts.start,
            size=opts.size
        )

        print(f"  {len(profiles)} profile(s) found")
        for profile in profiles:
            print(f"  Profile ID: {profile.profile_id}")
            print(f"    Name: {profile.profile_name}")
            print()

    def cmd_ca_profile_show(self, args):
        """Show profile details"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-show')
        parser.add_argument('profile_id', help='Profile ID')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)
        profile = profile_client.get_profile(opts.profile_id)

        print(f"  Profile ID: {profile.profile_id}")
        print(f"  Name: {profile.name}")
        print(f"  Description: {profile.description}")
        print(f"  Enabled: {profile.enabled}")
        print(f"  Visible: {profile.visible}")

    def cmd_ca_authority_find(self, args):
        """List authorities (sub-CAs)"""
        authority_client = AuthorityClient(self.ca_client)
        authorities = authority_client.list_cas()

        print(f"  {len(authorities)} CA(s) found")
        for ca in authorities:
            print(f"  Authority ID: {ca.aid}")
            print(f"    DN: {ca.dn}")
            print(f"    Enabled: {ca.enabled}")
            print()

    def cmd_ca_authority_show(self, args):
        """Show authority details"""
        parser = argparse.ArgumentParser(prog='pki ca-authority-show')
        parser.add_argument('authority_id', help='Authority ID')

        opts = parser.parse_args(args)

        authority_client = AuthorityClient(self.ca_client)
        ca = authority_client.get_ca(opts.authority_id)

        print(f"  Authority ID: {ca.aid}")
        print(f"  DN: {ca.dn}")
        print(f"  Description: {ca.description}")
        print(f"  Enabled: {ca.enabled}")
        print(f"  Is Host Authority: {ca.is_host_authority}")
        if ca.parent_aid:
            print(f"  Parent ID: {ca.parent_aid}")

    def cmd_ca_authority_create(self, args):
        """Create a sub-CA"""
        parser = argparse.ArgumentParser(prog='pki ca-authority-create')
        parser.add_argument('--dn', required=True,
                            help='Subject DN for the sub-CA')
        parser.add_argument('--description', required=True,
                            help='Description')
        parser.add_argument('--parent', required=True,
                            help='Parent authority ID')

        opts = parser.parse_args(args)

        ca_data = AuthorityData(
            dn=opts.dn,
            description=opts.description,
            parent_aid=opts.parent,
        )

        authority_client = AuthorityClient(self.ca_client)
        new_ca = authority_client.create_ca(ca_data)

        print(f"  Created authority {new_ca.aid}")
        print(f"  DN: {new_ca.dn}")

    def cmd_ca_authority_disable(self, args):
        """Disable a sub-CA"""
        parser = argparse.ArgumentParser(prog='pki ca-authority-disable')
        parser.add_argument('authority_id', help='Authority ID')

        opts = parser.parse_args(args)

        authority_client = AuthorityClient(self.ca_client)
        authority_client.disable_ca(opts.authority_id)

        print(f"  Disabled authority {opts.authority_id}")

    def cmd_ca_authority_enable(self, args):
        """Enable a sub-CA"""
        parser = argparse.ArgumentParser(prog='pki ca-authority-enable')
        parser.add_argument('authority_id', help='Authority ID')

        opts = parser.parse_args(args)

        authority_client = AuthorityClient(self.ca_client)
        authority_client.enable_ca(opts.authority_id)

        print(f"  Enabled authority {opts.authority_id}")

    def cmd_ca_authority_del(self, args):
        """Delete a sub-CA"""
        parser = argparse.ArgumentParser(prog='pki ca-authority-del')
        parser.add_argument('authority_id', help='Authority ID')

        opts = parser.parse_args(args)

        authority_client = AuthorityClient(self.ca_client)
        authority_client.delete_ca(opts.authority_id)

        print(f"  Deleted authority {opts.authority_id}")

    def cmd_ca_cert_request_review(self, args):
        """Review certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-review')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        review = cert_client.review_request(opts.request_id)

        print(f"  Request ID: {review.request_id}")
        print(f"  Request Type: {review.request_type}")
        print(f"  Request Status: {review.request_status}")
        if hasattr(review, 'profile_id') and review.profile_id:
            print(f"  Profile ID: {review.profile_id}")

    def cmd_ca_cert_request_validate(self, args):
        """Validate certificate request"""
        parser = argparse.ArgumentParser(
            prog='pki ca-cert-request-validate')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.validate_request(opts.request_id, None)

        print(f"  Validated request {opts.request_id}")

    def cmd_ca_cert_request_update(self, args):
        """Update certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-update')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.update_request(opts.request_id, None)

        print(f"  Updated request {opts.request_id}")

    def cmd_ca_cert_request_assign(self, args):
        """Assign certificate request"""
        parser = argparse.ArgumentParser(prog='pki ca-cert-request-assign')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.assign_request(opts.request_id, None)

        print(f"  Assigned request {opts.request_id}")

    def cmd_ca_cert_request_unassign(self, args):
        """Unassign certificate request"""
        parser = argparse.ArgumentParser(
            prog='pki ca-cert-request-unassign')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        cert_client.unassign_request(opts.request_id, None)

        print(f"  Unassigned request {opts.request_id}")

    def cmd_ca_cert_request_profile_find(self, args):
        """List enrollment templates"""
        parser = argparse.ArgumentParser(
            prog='pki ca-cert-request-profile-find')
        parser.add_argument('--start', type=int)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        profiles = cert_client.list_enrollment_templates(
            start=opts.start,
            size=opts.size
        )

        print(f"  {len(profiles)} enrollment template(s) found")
        for profile in profiles:
            print(f"  Profile ID: {profile.profile_id}")
            print(f"    Name: {profile.profile_name}")
            print()

    def cmd_ca_cert_request_profile_show(self, args):
        """Get enrollment template"""
        parser = argparse.ArgumentParser(
            prog='pki ca-cert-request-profile-show')
        parser.add_argument('profile_id', help='Profile ID')

        opts = parser.parse_args(args)

        cert_client = CACertClient(self.ca_client)
        template = cert_client.get_enrollment_template(opts.profile_id)

        print(f"  Profile ID: {template.profile_id}")
        if template.inputs:
            print("  Inputs:")
            for profile_input in template.inputs:
                for attribute in profile_input.attributes:
                    print(f"    {attribute.name}: {attribute.value or ''}")

    def cmd_ca_profile_add(self, args):
        """Add a certificate profile"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-add')
        parser.add_argument('file', help='Profile data file (JSON/XML)')
        parser.add_argument('--raw', action='store_true',
                            help='Send raw format')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)

        if opts.raw:
            profile = profile_client.create_profile_from_file(opts.file)
        else:
            from pki.profile import Profile
            profile_data = Profile.get_profile_data_from_file(opts.file)
            profile = profile_client.create_profile(profile_data)

        print(f"  Created profile {profile.profile_id}")

    def cmd_ca_profile_mod(self, args):
        """Modify a certificate profile"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-mod')
        parser.add_argument('file', help='Profile data file (JSON/XML)')
        parser.add_argument('--raw', action='store_true',
                            help='Send raw format')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)

        if opts.raw:
            profile = profile_client.modify_profile_from_file(opts.file)
        else:
            from pki.profile import Profile
            profile_data = Profile.get_profile_data_from_file(opts.file)
            profile = profile_client.modify_profile(profile_data)

        print(f"  Modified profile {profile.profile_id}")

    def cmd_ca_profile_del(self, args):
        """Delete a certificate profile"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-del')
        parser.add_argument('profile_id', help='Profile ID')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)
        profile_client.delete_profile(opts.profile_id)

        print(f"  Deleted profile {opts.profile_id}")

    def cmd_ca_profile_enable(self, args):
        """Enable a certificate profile"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-enable')
        parser.add_argument('profile_id', help='Profile ID')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)
        profile_client.enable_profile(opts.profile_id)

        print(f"  Enabled profile {opts.profile_id}")

    def cmd_ca_profile_disable(self, args):
        """Disable a certificate profile"""
        parser = argparse.ArgumentParser(prog='pki ca-profile-disable')
        parser.add_argument('profile_id', help='Profile ID')

        opts = parser.parse_args(args)

        profile_client = ProfileClient(self.ca_client)
        profile_client.disable_profile(opts.profile_id)

        print(f"  Disabled profile {opts.profile_id}")

    def cmd_securitydomain_show(self, args):
        """Show security domain info"""
        sd_client = SecurityDomainClient(self.ca_client)
        info = sd_client.get_domain_info()

        print(f"  Domain: {info.id}")
        for subsystem_type, subsystem in info.subsystems.items():
            print(f"  {subsystem_type}:")
            for host in subsystem.hosts.values():
                print(f"    {host.Hostname}:{host.SecurePort}")

    def cmd_kra_key_find(self, args):
        """Find keys"""
        parser = argparse.ArgumentParser(prog='pki kra-key-find')
        parser.add_argument('--clientKeyID', help='Client key ID')
        parser.add_argument('--status', help='Key status')
        parser.add_argument('--start', type=int)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        key_client = KeyClient(self.ca_client)
        keys = key_client.list_keys(
            client_key_id=opts.clientKeyID,
            status=opts.status,
            max_results=opts.size,
            start=opts.start,
            size=opts.size
        )

        for key_info in keys.key_infos:
            print(f"  Key ID: {key_info.get_key_id()}")
            print(f"    Client Key ID: {key_info.client_key_id}")
            print(f"    Status: {key_info.status}")
            print(f"    Algorithm: {key_info.algorithm}")
            print(f"    Size: {key_info.size}")
            print()

    def cmd_kra_key_show(self, args):
        """Show key info"""
        parser = argparse.ArgumentParser(prog='pki kra-key-show')
        parser.add_argument('key_id', help='Key ID')

        opts = parser.parse_args(args)

        key_client = KeyClient(self.ca_client)
        key_info = key_client.get_key_info(opts.key_id)

        print(f"  Key ID: {key_info.get_key_id()}")
        print(f"  Client Key ID: {key_info.client_key_id}")
        print(f"  Status: {key_info.status}")
        print(f"  Algorithm: {key_info.algorithm}")
        print(f"  Size: {key_info.size}")

    def cmd_kra_key_request_find(self, args):
        """Find key requests"""
        parser = argparse.ArgumentParser(prog='pki kra-key-request-find')
        parser.add_argument('--state', help='Request state')
        parser.add_argument('--type', dest='request_type',
                            help='Request type')
        parser.add_argument('--start', type=int)
        parser.add_argument('--size', type=int, default=20)

        opts = parser.parse_args(args)

        key_client = KeyClient(self.ca_client)
        requests = key_client.list_requests(
            request_state=opts.state,
            request_type=opts.request_type,
            start=opts.start,
            page_size=opts.size,
            max_results=opts.size
        )

        for req in requests.key_requests:
            print(f"  Request ID: {req.get_request_id()}")
            print(f"    Type: {req.request_type}")
            print(f"    Status: {req.request_status}")
            print()

    def cmd_kra_key_request_show(self, args):
        """Show key request info"""
        parser = argparse.ArgumentParser(prog='pki kra-key-request-show')
        parser.add_argument('request_id', help='Request ID')

        opts = parser.parse_args(args)

        key_client = KeyClient(self.ca_client)
        req = key_client.get_request_info(opts.request_id)

        print(f"  Request ID: {req.get_request_id()}")
        print(f"  Type: {req.request_type}")
        print(f"  Status: {req.request_status}")
        if req.get_key_id():
            print(f"  Key ID: {req.get_key_id()}")

    def cmd_kra_key_archive(self, args):
        """Archive a key"""
        parser = argparse.ArgumentParser(prog='pki kra-key-archive')
        parser.add_argument('--clientKeyID', required=True,
                            help='Client key ID')
        parser.add_argument('--algorithmOID', help='Algorithm OID')

        parser.parse_args(args)

        print("KRA key archival not yet implemented")
        return 1

    def cmd_kra_key_retrieve(self, args):
        """Retrieve a key"""
        parser = argparse.ArgumentParser(prog='pki kra-key-retrieve')
        parser.add_argument('--keyID', required=True, help='Key ID')

        parser.parse_args(args)

        print("KRA key retrieval not yet implemented")
        return 1

    def cmd_pkcs12_import(self, args, global_args):
        """Import PKCS#12 file into NSS database"""
        parser = argparse.ArgumentParser(prog='pki pkcs12-import')
        parser.add_argument('--pkcs12',
                            help='PKCS#12 file to import')
        parser.add_argument('--pkcs12-file', dest='pkcs12',
                            help='PKCS#12 file (deprecated, use --pkcs12)')
        parser.add_argument('--password',
                            help='PKCS#12 password')
        parser.add_argument('--password-file',
                            help='File containing PKCS#12 password')
        parser.add_argument('--pkcs12-password', dest='password',
                            help='PKCS#12 password (deprecated)')
        parser.add_argument('--pkcs12-password-file', dest='password_file',
                            help='PKCS#12 password file (deprecated)')
        parser.add_argument('--no-trust-flags', action='store_true',
                            help='Do not include trust flags')
        parser.add_argument('--no-user-certs', action='store_true',
                            help='Do not import user certificates')
        parser.add_argument('--no-ca-certs', action='store_true',
                            help='Do not import CA certificates')
        parser.add_argument('--overwrite', action='store_true',
                            help='Overwrite existing certificates')
        parser.add_argument('nicknames', nargs='*',
                            help='Nicknames to import (default: all)')

        opts = parser.parse_args(args)

        if not opts.pkcs12:
            print("Error: --pkcs12 is required", file=sys.stderr)
            return 1

        nssdb = global_args.nssdb
        if not nssdb:
            print("Error: NSS database location required (-d)",
                  file=sys.stderr)
            return 1

        if not os.path.isfile(opts.pkcs12):
            print(f"Error: PKCS#12 file not found: {opts.pkcs12}",
                  file=sys.stderr)
            return 1

        p12_password_file = opts.password_file
        if opts.password and not p12_password_file:
            fd, p12_password_file = tempfile.mkstemp(suffix='.pwd')
            with os.fdopen(fd, 'w') as f:
                f.write(opts.password)
            self._temp_files.append(p12_password_file)

        if not p12_password_file:
            print("Error: PKCS#12 password or password file required",
                  file=sys.stderr)
            return 1

        nssdb_pwd_file = self._get_nssdb_password_file(global_args)
        token = global_args.token

        cmd = ['pk12util', '-i', opts.pkcs12, '-d', nssdb]

        if nssdb_pwd_file:
            cmd.extend(['-k', nssdb_pwd_file])

        cmd.extend(['-w', p12_password_file])

        if token:
            cmd.extend(['-h', token])

        try:
            result = subprocess.run(
                cmd, check=True, capture_output=True, text=True)
            if self.verbose:
                if result.stdout:
                    print(result.stdout, end='')
                if result.stderr:
                    print(result.stderr, end='')
        except subprocess.CalledProcessError as e:
            print(f"Error: {e.stderr.strip()}", file=sys.stderr)
            return 1

        if opts.no_user_certs or opts.no_ca_certs:
            self._filter_imported_certs(
                nssdb, nssdb_pwd_file, token,
                opts.no_user_certs, opts.no_ca_certs)

        return 0

    def _filter_imported_certs(self, nssdb, pwd_file, token,
                               no_user_certs, no_ca_certs):
        """Remove unwanted certs after pk12util import."""
        cmd = ['certutil', '-L', '-d', nssdb]
        if token:
            cmd.extend(['-h', token])
        if pwd_file:
            cmd.extend(['-f', pwd_file])

        try:
            result = subprocess.run(
                cmd, check=True, capture_output=True, text=True)
        except subprocess.CalledProcessError:
            return

        for line in result.stdout.splitlines():
            if not line.strip() or line.startswith('Certificate Nickname'):
                continue
            parts = line.rsplit(None, 1)
            if len(parts) < 2:
                continue
            nickname = parts[0].strip()
            trust = parts[1]

            is_user = 'u' in trust
            is_ca = 'c' in trust.lower() or 'C' in trust

            if no_user_certs and is_user:
                self._delete_cert(nssdb, nickname, pwd_file)
            elif no_ca_certs and not is_user and is_ca:
                self._delete_cert(nssdb, nickname, pwd_file)

    def _delete_cert(self, nssdb, nickname, pwd_file):
        """Delete a certificate from the NSS database."""
        cmd = ['certutil', '-D', '-d', nssdb, '-n', nickname]
        if pwd_file:
            cmd.extend(['-f', pwd_file])
        subprocess.run(cmd, capture_output=True, check=False)

    def cmd_pkcs12_export(self, args, global_args):
        """Export certificates from NSS database to PKCS#12 file"""
        parser = argparse.ArgumentParser(prog='pki pkcs12-export')
        parser.add_argument('--pkcs12',
                            help='Output PKCS#12 file')
        parser.add_argument('--pkcs12-file', dest='pkcs12',
                            help='Output PKCS#12 file (deprecated)')
        parser.add_argument('--password',
                            help='PKCS#12 password')
        parser.add_argument('--password-file',
                            help='File containing PKCS#12 password')
        parser.add_argument('--pkcs12-password', dest='password',
                            help='PKCS#12 password (deprecated)')
        parser.add_argument('--pkcs12-password-file', dest='password_file',
                            help='PKCS#12 password file (deprecated)')
        parser.add_argument('--cert-encryption',
                            help='Certificate encryption algorithm')
        parser.add_argument('--key-encryption',
                            help='Key encryption algorithm')
        parser.add_argument('--append', action='store_true',
                            help='Append to existing PKCS#12 file')
        parser.add_argument('--no-trust-flags', action='store_true',
                            help='Do not include trust flags')
        parser.add_argument('--no-key', action='store_true',
                            help='Do not include private keys')
        parser.add_argument('--no-chain', action='store_true',
                            help='Do not include certificate chain')
        parser.add_argument('nicknames', nargs='*',
                            help='Nicknames to export (default: all)')

        opts = parser.parse_args(args)

        if not opts.pkcs12:
            print("Error: --pkcs12 is required", file=sys.stderr)
            return 1

        nssdb = global_args.nssdb
        if not nssdb:
            print("Error: NSS database location required (-d)",
                  file=sys.stderr)
            return 1

        p12_password_file = opts.password_file
        if opts.password and not p12_password_file:
            fd, p12_password_file = tempfile.mkstemp(suffix='.pwd')
            with os.fdopen(fd, 'w') as f:
                f.write(opts.password)
            self._temp_files.append(p12_password_file)

        if not p12_password_file:
            print("Error: PKCS#12 password or password file required",
                  file=sys.stderr)
            return 1

        nssdb_pwd_file = self._get_nssdb_password_file(global_args)
        token = global_args.token

        nicknames = opts.nicknames
        if not nicknames:
            nicknames = self._list_cert_nicknames(nssdb, nssdb_pwd_file, token)
            if not nicknames:
                print("Error: No certificates found in NSS database",
                      file=sys.stderr)
                return 1

        first = True
        for nickname in nicknames:
            cmd = ['pk12util', '-o', opts.pkcs12,
                   '-d', nssdb, '-n', nickname]

            if nssdb_pwd_file:
                cmd.extend(['-k', nssdb_pwd_file])

            cmd.extend(['-w', p12_password_file])

            if token:
                cmd.extend(['-h', token])

            if not first or opts.append:
                pass

            try:
                result = subprocess.run(
                    cmd, check=True, capture_output=True, text=True)
                if self.verbose:
                    if result.stdout:
                        print(result.stdout, end='')
                    if result.stderr:
                        print(result.stderr, end='')
            except subprocess.CalledProcessError as e:
                if opts.no_key:
                    continue
                print(f"Error exporting '{nickname}': {e.stderr.strip()}",
                      file=sys.stderr)
                return 1

            first = False

        return 0

    def _list_cert_nicknames(self, nssdb, pwd_file, token):
        """List certificate nicknames in the NSS database."""
        cmd = ['certutil', '-L', '-d', nssdb]
        if token:
            cmd.extend(['-h', token])
        if pwd_file:
            cmd.extend(['-f', pwd_file])

        try:
            result = subprocess.run(
                cmd, check=True, capture_output=True, text=True)
        except subprocess.CalledProcessError:
            return []

        nicknames = []
        for line in result.stdout.splitlines():
            if not line.strip() or line.startswith('Certificate Nickname'):
                continue
            if line.startswith(' ') and 'Trust Attributes' in line:
                continue
            parts = line.rsplit(None, 1)
            if len(parts) >= 2:
                nicknames.append(parts[0].strip())
        return nicknames

    def run(self, argv):
        """Main entry point"""
        global_portion, cmd_portion = self._split_at_command(argv[1:])
        global_args, extra = self.parse_global_args(global_portion)

        if global_args.version:
            print("ipacta 1.0.0 (Dogtag-compatible)")
            return 0

        remaining = extra + cmd_portion

        if global_args.help or not remaining:
            self.print_help()
            return 0

        self.verbose = global_args.verbose
        self.debug = global_args.debug

        command = remaining[0]
        cmd_args = remaining[1:]

        local_cmd_map = {
            'pkcs12-import': self.cmd_pkcs12_import,
            'pkcs12-export': self.cmd_pkcs12_export,
        }

        if command in local_cmd_map:
            return local_cmd_map[command](cmd_args, global_args) or 0

        self.create_connection(global_args)

        cmd_map = {
            'info': self.cmd_info,
            'ca-cert-find': self.cmd_ca_cert_find,
            'ca-cert-show': self.cmd_ca_cert_show,
            'ca-cert-revoke': self.cmd_ca_cert_revoke,
            'ca-cert-hold': self.cmd_ca_cert_hold,
            'ca-cert-release-hold': self.cmd_ca_cert_release_hold,
            'ca-cert-request-submit': self.cmd_ca_cert_request_submit,
            'ca-cert-request-find': self.cmd_ca_cert_request_find,
            'ca-cert-request-show': self.cmd_ca_cert_request_show,
            'ca-cert-request-review': self.cmd_ca_cert_request_review,
            'ca-cert-request-approve': self.cmd_ca_cert_request_approve,
            'ca-cert-request-reject': self.cmd_ca_cert_request_reject,
            'ca-cert-request-cancel': self.cmd_ca_cert_request_cancel,
            'ca-cert-request-validate': self.cmd_ca_cert_request_validate,
            'ca-cert-request-update': self.cmd_ca_cert_request_update,
            'ca-cert-request-assign': self.cmd_ca_cert_request_assign,
            'ca-cert-request-unassign': self.cmd_ca_cert_request_unassign,
            'ca-cert-request-profile-find':
                self.cmd_ca_cert_request_profile_find,
            'ca-cert-request-profile-show':
                self.cmd_ca_cert_request_profile_show,
            'ca-profile-find': self.cmd_ca_profile_find,
            'ca-profile-show': self.cmd_ca_profile_show,
            'ca-profile-add': self.cmd_ca_profile_add,
            'ca-profile-mod': self.cmd_ca_profile_mod,
            'ca-profile-del': self.cmd_ca_profile_del,
            'ca-profile-enable': self.cmd_ca_profile_enable,
            'ca-profile-disable': self.cmd_ca_profile_disable,
            'ca-authority-find': self.cmd_ca_authority_find,
            'ca-authority-show': self.cmd_ca_authority_show,
            'ca-authority-create': self.cmd_ca_authority_create,
            'ca-authority-disable': self.cmd_ca_authority_disable,
            'ca-authority-enable': self.cmd_ca_authority_enable,
            'ca-authority-del': self.cmd_ca_authority_del,
            'securitydomain-show': self.cmd_securitydomain_show,
            'kra-key-find': self.cmd_kra_key_find,
            'kra-key-show': self.cmd_kra_key_show,
            'kra-key-archive': self.cmd_kra_key_archive,
            'kra-key-retrieve': self.cmd_kra_key_retrieve,
            'kra-key-request-find': self.cmd_kra_key_request_find,
            'kra-key-request-show': self.cmd_kra_key_request_show,
        }

        if command in cmd_map:
            return cmd_map[command](cmd_args) or 0
        else:
            print(f"Error: Unknown command '{command}'", file=sys.stderr)
            print("Run 'pki --help' for usage", file=sys.stderr)
            return 1

    def print_help(self):
        """Print help message"""
        print("""Usage: pki [OPTIONS] <command> [command-options]

Connection Options:
  -d, --nssdb <path>            NSS database location
  -c, --password <password>     NSS database password
  -C, --password-file <file>    NSS database password file
  -n, --nickname <nickname>     Client certificate nickname
  -e, --cert-file <file>        PEM client certificate file
  -k, --key-file <file>         PEM client key file
  -u, --username <username>     Username for basic auth
  -w, --user-password <pass>    Password for basic auth
  -W, --user-password-file <f>  Password file for basic auth
  -U, --url <URL>               PKI server URL
  -P, --protocol <protocol>     Protocol (default: https)
  -f, --password-conf <file>    Password config file (password.conf)
      --token <token>           Security token name
  -h, --hostname <hostname>     Hostname (default: FQDN)
  -p, --port <port>             Port (default: 8443)

General Options:
  -v, --verbose                 Verbose mode
      --debug                   Debug mode
      --help                    Show help
      --version                 Show version

Commands:
  info                              Display server info

  ca-cert-find                      Find certificates
  ca-cert-show <serial>             Show certificate
  ca-cert-revoke <serial>           Revoke certificate
  ca-cert-hold <serial>             Place certificate on hold
  ca-cert-release-hold <serial>     Release certificate hold

  ca-cert-request-submit            Submit certificate request
  ca-cert-request-find              Find certificate requests
  ca-cert-request-show <id>         Show certificate request
  ca-cert-request-review <id>       Review certificate request
  ca-cert-request-approve <id>      Approve certificate request
  ca-cert-request-reject <id>       Reject certificate request
  ca-cert-request-cancel <id>       Cancel certificate request
  ca-cert-request-validate <id>     Validate certificate request
  ca-cert-request-update <id>       Update certificate request
  ca-cert-request-assign <id>       Assign certificate request
  ca-cert-request-unassign <id>     Unassign certificate request
  ca-cert-request-profile-find      List enrollment templates
  ca-cert-request-profile-show <id> Get enrollment template

  ca-profile-find                   List certificate profiles
  ca-profile-show <id>              Show profile
  ca-profile-add <file>             Add profile
  ca-profile-mod <file>             Modify profile
  ca-profile-del <id>               Delete profile
  ca-profile-enable <id>            Enable profile
  ca-profile-disable <id>           Disable profile

  ca-authority-find                 List sub-CAs
  ca-authority-show <id>            Show sub-CA
  ca-authority-create               Create sub-CA
  ca-authority-disable <id>         Disable sub-CA
  ca-authority-enable <id>          Enable sub-CA
  ca-authority-del <id>             Delete sub-CA

  pkcs12-import                     Import PKCS#12 file into NSS database
  pkcs12-export                     Export certs from NSS database to PKCS#12

  securitydomain-show               Show security domain

  kra-key-find                      Find keys
  kra-key-show <id>                 Show key info
  kra-key-archive                   Archive key
  kra-key-retrieve                  Retrieve key
  kra-key-request-find              Find key requests
  kra-key-request-show <id>         Show key request

Compatible with Dogtag PKI commands.
""")


def main():
    """Main entry point"""
    cli = PKICLIClient()
    try:
        sys.exit(cli.run(sys.argv))
    except KeyboardInterrupt:
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
