"""Settings API routes."""

import os

from flask import Blueprint, request, current_app
from flask_jwt_extended import jwt_required, get_jwt_identity
from werkzeug.utils import secure_filename

from shopdb.extensions import db, cache
from shopdb.core.models import Setting, AuditLog
from shopdb.utils.responses import success_response, error_response, ErrorCodes

from shopdb.utils.authz import require_permission, require_role
from shopdb.utils.uploads import send_upload

settings_bp = Blueprint('settings', __name__)

# Floor-map blueprint uploads live in the instance dir and are served publicly
# (the kiosk dashboards read them without auth).
MAP_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}

# Branding logo uploads allow everything a map image does plus .ico (favicons).
BRANDING_IMAGE_EXTENSIONS = MAP_IMAGE_EXTENSIONS | {'.ico'}

# Branding logo kinds and the setting key each one writes.
BRANDING_KIND_SETTINGS = {
    'site': 'site_logo',
    'qr': 'qr_logo',
    'badge': 'badge_logo',
    'favicon': 'site_favicon',
}


def _map_dir():
    return os.path.join(current_app.instance_path, 'maps')


def _branding_dir():
    return os.path.join(current_app.instance_path, 'branding')

# Cache key for settings
SETTINGS_CACHE_KEY = 'system_settings'
SETTINGS_CACHE_TTL = 300  # 5 minutes

# Placeholder returned in API responses for secret values so they are never
# exposed in plaintext. Sending it back on update is treated as "unchanged".
SECRET_MASK = '********'

# Public-settings allowlist. An UNAUTHENTICATED caller (kiosk dashboards, print
# pages, the login screen, the setup router) may read only these; everything
# else (smtp_host, employee_db_host, zabbix_url, servicenow URLs, ...) needs a
# valid token. Secrets stay masked in both cases. Keep in sync with the keys
# frontend/src/utils/siteSettings.js + mapConfig.js + setupState.js read before
# login. Whole categories that are purely presentation are allowed wholesale;
# the rest are named keys so a new integration key does not leak by default.
# A plugin adds its own public keys by declaring public=True in
# get_settings_defaults - core does not carry a list of every plugin's keys.
PUBLIC_SETTING_CATEGORIES = {'branding', 'map'}
PUBLIC_SETTING_KEYS = {
    'site_base_url', 'facility_name', 'printer_hostname_template',
    'contact_email_domain', 'servicenow_enabled', 'setup_complete',
    'site_timezone',
}


def _plugin_declared_settings() -> dict:
    """Declared settings ({key: entry}) of every loaded plugin, or {}."""
    pm = current_app.extensions.get('plugin_manager')
    if not pm:
        return {}
    try:
        return pm.get_declared_plugin_settings()
    except Exception:
        current_app.logger.exception('Could not read plugin setting declarations')
        return {}


def _is_public_setting(setting) -> bool:
    # A plugin declares its own public keys (get_settings_defaults, public=True)
    # so a kiosk or print page that renders before login can read them without
    # core carrying a list of every plugin's keys.
    if setting.category in PUBLIC_SETTING_CATEGORIES or setting.key in PUBLIC_SETTING_KEYS:
        return True
    return bool(_plugin_declared_settings().get(setting.key, {}).get('public'))

# Optional asset identifiers and the asset types they can be toggled on.
# Drives per-type seed keys and the Settings matrix UI. The asset type names
# match the AssetType.assettype values seeded by each plugin.
IDENTIFIER_LABELS = {
    'gaugelabreference': 'Gauge Lab Reference',
    'maintenancereference': 'Maintenance Reference',
    'fqdn': 'FQDN / hostname',
}
IDENTIFIER_ASSETTYPES = ['machine', 'computer', 'printer', 'network_device',
                         'measuring_tool']

# Global-search result types that can be toggled on/off independently of whether
# the owning plugin is enabled. Keys match the `type` field on search results;
# seed keys are search_<type>_enabled (boolean, default true). Drives the
# Settings "Search" toggles and the filter in shopdb/core/api/search.py.
SEARCH_DOMAINS = {
    'application': 'Applications',
    'knowledgebase': 'Knowledge Base',
    'employee': 'Employees',
    'machine': 'Machines',
    'computer': 'PCs',
    'printer': 'Printers',
    'network_device': 'Network Devices',
    'measuring_tool': 'Measuring Tools',
    'notification': 'Notifications',
    'subnet': 'Subnets',
    'usb_device': 'USB Devices',
    'printed_item': 'Printed Items',
}

def _declared_default(key: str) -> dict:
    """Return the declared default for a key (core defaults, then plugins).

    Empty dict when nobody declares it - a genuinely ad-hoc key.
    """
    for entry in build_default_settings():
        if entry['key'] == key:
            return entry
    return _plugin_declared_settings().get(key, {})


def _is_secret(key: str) -> bool:
    return 'password' in key or 'token' in key or 'secret' in key


def _serialize_setting(setting):
    """Serialize a setting, masking secret values so they never leave the API."""
    data = setting.to_dict()
    if _is_secret(setting.key):
        data['value'] = SECRET_MASK if setting.value else ''
    return data


def get_cached_settings():
    """Get all settings from cache or database."""
    cached = cache.get(SETTINGS_CACHE_KEY)
    if cached is not None:
        return cached

    settings = Setting.query.all()
    result = {s.key: s.get_typed_value() for s in settings}
    cache.set(SETTINGS_CACHE_KEY, result, timeout=SETTINGS_CACHE_TTL)
    return result


def invalidate_settings_cache():
    """Clear the settings cache."""
    cache.delete(SETTINGS_CACHE_KEY)


@settings_bp.route('/map-blueprint', methods=['POST'])
@jwt_required()
@require_role('admin')
def upload_map_blueprint():
    """Upload a floor-map blueprint image and point the setting at it.

    multipart/form-data: file=<image>, theme=light|dark. Saves to the instance
    maps dir and sets map_blueprint_<theme> to the served URL.
    """
    theme = (request.form.get('theme') or '').strip().lower()
    if theme not in ('light', 'dark'):
        return error_response(ErrorCodes.VALIDATION_ERROR, 'theme must be light or dark')
    upload = request.files.get('file')
    if not upload or not upload.filename:
        return error_response(ErrorCodes.VALIDATION_ERROR, 'No file provided')
    ext = os.path.splitext(upload.filename)[1].lower()
    if ext not in MAP_IMAGE_EXTENSIONS:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              f'Unsupported image type {ext}')

    os.makedirs(_map_dir(), exist_ok=True)
    filename = secure_filename(f'blueprint-{theme}{ext}')
    upload.save(os.path.join(_map_dir(), filename))

    url = f'/api/settings/map-blueprint/{filename}'
    key = f'map_blueprint_{theme}'
    setting = Setting.query.filter_by(key=key).first()
    if setting:
        setting.value = url
    else:
        db.session.add(Setting(key=key, value=url, valuetype='string', category='map'))
    db.session.commit()
    invalidate_settings_cache()
    return success_response({'key': key, 'value': url}, message='Blueprint uploaded')


@settings_bp.route('/map-blueprint/<path:filename>', methods=['GET'])
def serve_map_blueprint(filename):
    """Serve an uploaded blueprint image (public - kiosks read it)."""
    return send_upload(_map_dir(), filename)


@settings_bp.route('/branding-logo', methods=['POST'])
@jwt_required()
@require_role('admin')
def upload_branding_logo():
    """Upload a branding logo image and point the matching setting at it.

    multipart/form-data: file=<image>, kind=site|qr|badge|favicon. Saves to the
    instance branding dir and sets the matching branding setting to the served
    URL. .ico is accepted in addition to the map image types (for favicons).
    """
    kind = (request.form.get('kind') or '').strip().lower()
    if kind not in BRANDING_KIND_SETTINGS:
        return error_response(
            ErrorCodes.VALIDATION_ERROR,
            'kind must be one of: ' + ', '.join(sorted(BRANDING_KIND_SETTINGS)))
    upload = request.files.get('file')
    if not upload or not upload.filename:
        return error_response(ErrorCodes.VALIDATION_ERROR, 'No file provided')
    ext = os.path.splitext(upload.filename)[1].lower()
    if ext not in BRANDING_IMAGE_EXTENSIONS:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              f'Unsupported image type {ext}')

    os.makedirs(_branding_dir(), exist_ok=True)
    filename = secure_filename(f'logo-{kind}{ext}')
    upload.save(os.path.join(_branding_dir(), filename))

    url = f'/api/settings/branding/{filename}'
    key = BRANDING_KIND_SETTINGS[kind]
    setting = Setting.query.filter_by(key=key).first()
    if setting:
        setting.value = url
    else:
        db.session.add(Setting(key=key, value=url, valuetype='string', category='branding'))
    db.session.commit()
    invalidate_settings_cache()
    return success_response({'key': key, 'value': url}, message='Logo uploaded')


@settings_bp.route('/branding/<path:filename>', methods=['GET'])
def serve_branding_logo(filename):
    """Serve an uploaded branding logo (public - kiosks/print pages read it)."""
    return send_upload(_branding_dir(), filename)


@settings_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_settings():
    """List all settings, optionally filtered by category."""
    category = request.args.get('category')

    query = Setting.query
    if category:
        query = query.filter_by(category=category)

    settings = query.order_by(Setting.category, Setting.key).all()
    # Unauthenticated callers see only the public allowlist (branding + a few
    # bootstrap keys); an authed principal sees everything (secrets masked).
    if get_jwt_identity() is None:
        settings = [s for s in settings if _is_public_setting(s)]
    return success_response([_serialize_setting(s) for s in settings])


@settings_bp.route('/<key>', methods=['GET'])
@jwt_required(optional=True)
def get_setting(key: str):
    """Get a single setting by key."""
    setting = Setting.query.filter_by(key=key).first()

    if not setting:
        return error_response(ErrorCodes.NOT_FOUND, f'Setting {key} not found', http_code=404)

    # A non-public key is invisible to an unauthenticated caller (404, not 403,
    # so its existence is not confirmed either).
    if get_jwt_identity() is None and not _is_public_setting(setting):
        return error_response(ErrorCodes.NOT_FOUND, f'Setting {key} not found', http_code=404)

    return success_response(_serialize_setting(setting))


@settings_bp.route('/<key>', methods=['PUT'])
@jwt_required()
@require_permission('settings.edit')
def update_setting(key: str):
    """Update a setting value."""
    data = request.get_json()

    if data is None or 'value' not in data:
        return error_response(ErrorCodes.VALIDATION_ERROR, 'value is required')

    setting = Setting.query.filter_by(key=key).first()

    # Upsert: create the row on first write (e.g. plugin config keys the setup
    # wizard saves). Take the category, type and description from whoever
    # declares the key - core defaults or a plugin's get_settings_defaults.
    # Filing a declared key under a placeholder category hid it from the
    # settings page that had just written it, which read as "did not save".
    if not setting:
        declared = _declared_default(key)
        setting = Setting(
            key=key,
            value='',
            valuetype=declared.get('valuetype', 'string'),
            category=declared.get('category', 'plugin'),
            description=declared.get('description'),
        )
        db.session.add(setting)

    # Track old value for audit
    old_value = setting.value

    value = data['value']

    # A secret submitted as the mask placeholder means "leave unchanged" - the
    # client only ever received the mask, so don't overwrite the real secret.
    if _is_secret(key) and value == SECRET_MASK:
        return success_response(_serialize_setting(setting), message='Setting unchanged')

    # Convert value to string for storage
    if isinstance(value, bool):
        setting.value = 'true' if value else 'false'
    else:
        setting.value = str(value) if value is not None else None

    # Audit log (mask sensitive values)
    is_sensitive = _is_secret(key)
    AuditLog.log('updated', 'Setting', entityname=key, changes={
        'value': {
            'old': '***' if is_sensitive else old_value,
            'new': '***' if is_sensitive else setting.value
        }
    })

    db.session.commit()
    invalidate_settings_cache()

    return success_response(_serialize_setting(setting), message='Setting updated')


@settings_bp.route('/test-email', methods=['POST'])
@jwt_required()
@require_permission('settings.edit')
def test_email():
    """Send a test email to verify SMTP configuration.

    Request: { "to": "addr@example.com" }  (falls back to alert_recipients)

    Returns a 200 with a `sent` flag either way. When SMTP is not configured
    the response explains that gracefully; when a real send fails the SMTP error
    is surfaced with any credential scrubbed out.
    """
    from shopdb.utils.mailer import get_smtp_config, render_email, try_send

    data = request.get_json() or {}
    config = get_smtp_config()
    recipient = data.get('to') or config['alert_recipients']

    if not config['enabled'] or not config['host']:
        return success_response(
            {'sent': False, 'reason': 'notconfigured'},
            message='Email is not configured (SMTP disabled or host unset).')
    if not recipient:
        return error_response(
            ErrorCodes.VALIDATION_ERROR,
            'No recipient. Provide "to" or set Alert Recipients.')

    html, text = render_email(
        'ShopDB test email',
        '<p>This is a test message confirming your SMTP settings work.</p>')
    ok, error = try_send(recipient, 'ShopDB test email', html, text=text)
    if ok:
        return success_response({'sent': True}, message='Test email sent.')
    return success_response(
        {'sent': False, 'error': error},
        message='Test email failed: ' + (error or 'unknown error'))


@settings_bp.route('', methods=['POST'])
@jwt_required()
@require_permission('settings.edit')
def create_setting():
    """Create a new setting (admin only)."""
    data = request.get_json()

    if not data or not data.get('key'):
        return error_response(ErrorCodes.VALIDATION_ERROR, 'key is required')

    if Setting.query.filter_by(key=data['key']).first():
        return error_response(ErrorCodes.CONFLICT, f"Setting '{data['key']}' already exists", http_code=409)

    value = data.get('value')
    if isinstance(value, bool):
        value_str = 'true' if value else 'false'
    else:
        value_str = str(value) if value is not None else None

    setting = Setting(
        key=data['key'],
        value=value_str,
        valuetype=data.get('valuetype', 'string'),
        category=data.get('category', 'general'),
        description=data.get('description')
    )

    db.session.add(setting)
    db.session.commit()
    invalidate_settings_cache()

    return success_response(setting.to_dict(), message='Setting created', http_code=201)


def build_default_settings():
    """Return the full default-settings list (identifier toggles + static).

    Shared by the /settings/seed route and the `flask seed settings` CLI so
    the two definitions never drift.
    """
    # Asset identifier feature toggles, per identifier AND per asset type.
    # Key format: identifier_<name>_<assettype>_enabled (boolean). Admins pick
    # which optional identifiers show on which asset types. See ADR-001.
    identifierdefaults = [
        {
            'key': f'identifier_{name}_{assettype}_enabled',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'identifiers',
            'description': f'Show the {label} identifier on {assettype} assets',
        }
        for name, label in IDENTIFIER_LABELS.items()
        for assettype in IDENTIFIER_ASSETTYPES
    ]

    # Per-domain global-search toggles (search_<type>_enabled).
    searchdefaults = [
        {
            'key': f'search_{key}_enabled',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'search',
            'description': f'Include {label} in global search results',
        }
        for key, label in SEARCH_DOMAINS.items()
    ]

    # Facility floor-map blueprint. Each site instance (ADR-004) points these
    # at its own floor-plan image and pixel dimensions; the map frontend reads
    # them instead of hardcoding one facility's plan. Defaults are a generic
    # placeholder floor plan so an un-reconfigured install still renders.
    mapdefaults = [
        {
            'key': 'map_blueprint_light',
            'value': '/static/images/floorplan-placeholder.svg',
            'valuetype': 'string',
            'category': 'map',
            'description': 'Floor-map blueprint image (light theme) for this facility'
        },
        {
            'key': 'map_blueprint_dark',
            'value': '/static/images/floorplan-placeholder.svg',
            'valuetype': 'string',
            'category': 'map',
            'description': 'Floor-map blueprint image (dark theme) for this facility'
        },
        {
            'key': 'map_width',
            'value': '3300',
            'valuetype': 'integer',
            'category': 'map',
            'description': 'Floor-map blueprint width in pixels (native size of the image)'
        },
        {
            'key': 'map_height',
            'value': '2550',
            'valuetype': 'integer',
            'category': 'map',
            'description': 'Floor-map blueprint height in pixels (native size of the image)'
        },
    ]

    # Site identity. Each instance (ADR-004) sets its own public URL - used for
    # QR codes and any absolute link the app emits - and facility name shown on
    # the shopfloor dashboard. Blank site_base_url falls back to the browsing
    # origin so nothing breaks before a site configures it.
    sitedefaults = [
        {
            'key': 'setup_complete',
            'value': 'false',
            'valuetype': 'boolean',
            'category': 'site',
            'description': 'Set true once the first-run setup wizard has been finished'
        },
        {
            'key': 'site_timezone',
            'value': 'America/New_York',
            'valuetype': 'string',
            'category': 'site',
            'description': "IANA timezone for this site (e.g. America/New_York, America/Chicago). Notification start/end times and daily-reset expiry are computed in this zone."
        },
        {
            'key': 'employee_directory_mode',
            'value': 'selfhosted',
            'valuetype': 'string',
            'category': 'site',
            'description': "Employee directory source: 'selfhosted' (tables in this app, default) or 'external' (a separate HR database)"
        },
        {
            'key': 'usb_directory_mode',
            'value': 'selfhosted',
            'valuetype': 'string',
            'category': 'site',
            'description': "USB check-in/out source: 'selfhosted' (tables in this app, default) or 'external' (a separate cmmc_usb database)"
        },
        {
            'key': 'site_base_url',
            'value': '',
            'valuetype': 'string',
            'category': 'site',
            'description': 'Public base URL of this site (scheme + host), e.g. https://shopdb.example.net. Used for QR codes and absolute links. Blank = use the browsing origin.'
        },
        {
            'key': 'facility_name',
            'value': '',
            'valuetype': 'string',
            'category': 'site',
            'description': 'Facility name shown on the shopfloor dashboard header (blank = frontend falls back to ShopDB)'
        },
        {
            'key': 'pc_access_domain',
            'value': 'device.geaerospace.net',  # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
            'valuetype': 'string',
            'category': 'site',
            'description': 'Domain appended to a PC hostname to build remote-access links (host.device.geaerospace.net). Blank = use the hostname as-is.'  # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
        },
        {
            'key': 'employeeid_pattern',
            'value': r'^\d{9}$',
            'valuetype': 'string',
            'category': 'site',
            'description': 'Regex a search term must match to be treated as an employee id. Invalid regex falls back to the default and never errors.'
        },
        {
            'key': 'dualpath_single_machine',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'site',
            'description': 'Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in lists, counts, and the floor map. The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. Off = list and count both bays separately.'
        },
        {
            'key': 'printer_hostname_template',
            'value': 'Printer-{ip}.printer.geaerospace.net',  # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
            'valuetype': 'string',
            'category': 'site',
            'description': 'Template for a printer hostname built from its IP. {ip} is the dash-separated IP address.'
        },
        {
            'key': 'contact_email_domain',
            'value': 'geaerospace.com',
            'valuetype': 'string',
            'category': 'site',
            'description': 'Email domain appended to a contact SSO to build email/Teams links (sso@domain). Blank disables contact action buttons.'
        },
    ]

    # Site branding. Each instance (ADR-004) can replace the shipped GE defaults
    # with its own logos, favicon, and primary color. Blank values fall back to
    # the built-in shipped assets so an un-reconfigured install still renders.
    brandingdefaults = [
        {
            'key': 'site_logo',
            'value': '/ge-aerospace-logo.svg',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Main site logo shown in the app header and login page'
        },
        {
            'key': 'qr_logo',
            'value': '/ge-monogram.svg',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Logo composited in the center of printer QR labels (blank = no QR overlay)'
        },
        {
            'key': 'badge_logo',
            'value': '/ge-aerospace-logo.svg',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Logo shown on the machine badge print page'
        },
        {
            'key': 'site_favicon',
            'value': '',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Browser tab favicon (blank = shipped /favicon.svg)'
        },
        {
            'key': 'brand_primary_color',
            'value': '',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Primary brand color as a CSS color value (blank = built-in theme color)'
        },
        {
            'key': 'brand_primary_dark_color',
            'value': '',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Primary hover/active color (blank = derived by darkening the primary color ~15%)'
        },
        {
            'key': 'brand_accent_color',
            'value': '',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Accent color for secondary buttons and badges (blank = built-in theme color)'
        },
        {
            'key': 'brand_sidebar_color',
            'value': '',
            'valuetype': 'string',
            'category': 'branding',
            'description': 'Sidebar background color (blank = built-in theme color)'
        },
    ]

    # Printed QR/label targets. Blank template = QR links to the asset's own
    # detail page on this instance; a non-blank value is a URL template with
    # {placeholder} substitution so a site can point labels anywhere.
    printingdefaults = [
        {
            'key': 'qr_target_printer',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for printer QR labels. Blank = link to the printer page. Placeholders: {printerid}, {assetid}, {assetnumber}, {serialnumber}, {ip}, {hostname}.'
        },
        {
            'key': 'qr_target_usb',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for USB label QR codes. Blank = link to the USB device page. Placeholders: {id}, {serialnumber}, {alias}.'
        },
        {
            'key': 'usb_label_style',
            'value': 'barcode',
            'valuetype': 'string',
            'category': 'printing',
            'description': "USB mini-label code style: 'barcode' (CODE128 of the serial number) or 'qr' (QR code linking to the QR target)."
        },
        {
            'key': 'qr_target_machine',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for machine labels. Blank = link to the machine page. Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}.'
        },
        {
            'key': 'qr_target_computer',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for computer labels. Blank = link to the computer page. Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}.'
        },
        {
            'key': 'qr_target_network_device',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for network-device labels. Blank = link to the device page. Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}.'
        },
        {
            'key': 'qr_target_measuring_tool',
            'value': '',
            'valuetype': 'string',
            'category': 'printing',
            'description': 'Custom URL template for measuring-tool labels. Blank = link to the tool page. Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}, {locationcode}, {locationname}.'
        },
        {
            'key': 'label_default_style',
            'value': 'card',
            'valuetype': 'string',
            'category': 'printing',
            'description': "Default asset-label layout: 'card' (badge with image and identity) or 'plain' (just the code and a caption)."
        },
        {
            'key': 'label_default_codetype',
            'value': 'qr',
            'valuetype': 'string',
            'category': 'printing',
            'description': "Default asset-label code type: 'qr' (QR code) or 'barcode' (CODE128)."
        },
    ]

    # Per-asset-type default for what a label's code encodes. Machines default
    # to their machine number (assetnumber), measuring tools to their inspection
    # location code, everything else to a link to the asset page. Values:
    # assetpage | assetnumber | serialnumber | location | custom.
    labelencodesdefaults = {
        'machine': 'assetnumber',
        'computer': 'assetpage',
        'printer': 'assetpage',
        'network_device': 'assetpage',
        'measuring_tool': 'location',
    }
    printingdefaults += [
        {
            'key': f'label_default_encodes_{assettype}',
            'value': value,
            'valuetype': 'string',
            'category': 'printing',
            'description': f'What a {assettype} label encodes by default: assetpage, '
                           'assetnumber, serialnumber'
                           + (', location' if assettype == 'measuring_tool' else '')
                           + ', or custom.',
        }
        for assettype, value in labelencodesdefaults.items()
    ]

    # Collector pc-type -> ComputerType mapping is computers-plugin domain;
    # the plugin seeds pctypemap_<pxetype> settings on install.
    defaults = sitedefaults + brandingdefaults + printingdefaults + identifierdefaults + searchdefaults + mapdefaults + [
        # ServiceNow ticket links. Each instance points these at its own
        # ServiceNow tenant; blank/disabled renders tickets as plain text.
        {
            'key': 'servicenow_enabled',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'integrations',
            'description': 'Enable ServiceNow ticket recognition and links in search and dashboards'
        },
        {
            'key': 'servicenow_search_url',
            'value': (
                'https://geaerospaceqa.service-now.com/now/nav/ui/search/'
                '0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/'
                'global-search-data-config-id/c861cea2c7022010099a308dc7c26041/'
            ),
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'ServiceNow global-search URL template. {ticket} is the ticket number.'
        },
        {
            'key': 'servicenow_ticket_prefixes',
            'value': 'GEINC,GECHG,GERIT,GESCT',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Comma-separated ticket-number prefixes recognized as ServiceNow tickets'
        },
        {
            'key': 'servicenow_incident_url',
            'value': 'https://geaerospaceqa.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'ServiceNow incident URL template. {ticket} is the incident number.'
        },
        {
            'key': 'servicenow_change_url',
            'value': 'https://geaerospaceqa.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'ServiceNow change-request URL template. {ticket} is the change number.'
        },
        # Zabbix integration
        {
            'key': 'zabbix_enabled',
            'value': 'false',
            'valuetype': 'boolean',
            'category': 'integrations',
            'description': 'Enable Zabbix integration for printer supply monitoring'
        },
        {
            'key': 'zabbix_url',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Zabbix API URL (e.g., http://zabbix.example.com:8080)'
        },
        {
            'key': 'zabbix_token',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Zabbix API authentication token'
        },
        # Dell warranty lookup (Dell TechDirect Warranty API, OAuth2)
        {
            'key': 'warranty_dell_enabled',
            'value': 'false',
            'valuetype': 'boolean',
            'category': 'integrations',
            'description': 'Enable Dell warranty lookups (service-tag entitlements)'
        },
        {
            'key': 'warranty_dell_clientid',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Dell TechDirect API client id'
        },
        {
            'key': 'warranty_dell_clientsecret',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Dell TechDirect API client secret'
        },
        {
            'key': 'warranty_dell_tokenurl',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Dell OAuth token URL (blank = Dell default)'
        },
        {
            'key': 'warranty_dell_apiurl',
            'value': '',
            'valuetype': 'string',
            'category': 'integrations',
            'description': 'Dell warranty API URL (blank = Dell default)'
        },
        # Email/SMTP settings
        {
            'key': 'smtp_enabled',
            'value': 'false',
            'valuetype': 'boolean',
            'category': 'email',
            'description': 'Enable email notifications and alerts'
        },
        {
            'key': 'smtp_host',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'SMTP server hostname'
        },
        {
            'key': 'smtp_port',
            'value': '587',
            'valuetype': 'integer',
            'category': 'email',
            'description': 'SMTP server port (usually 587 for TLS, 465 for SSL, 25 for unencrypted)'
        },
        {
            'key': 'smtp_username',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'SMTP authentication username'
        },
        {
            'key': 'smtp_password',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'SMTP authentication password'
        },
        {
            'key': 'smtp_use_tls',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'email',
            'description': 'Use TLS encryption for SMTP connection'
        },
        {
            'key': 'smtp_from_address',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'From address for outgoing emails'
        },
        {
            'key': 'smtp_from_name',
            'value': 'ShopDB',
            'valuetype': 'string',
            'category': 'email',
            'description': 'From name for outgoing emails'
        },
        {
            'key': 'alert_recipients',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'Default email recipients for alerts (comma-separated)'
        },
        {
            'key': 'site_base_url',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'Public base URL of this site (e.g. '
                           'https://host/ops), used to build links in emails'
        },
        {
            'key': 'alert_webhook_url',
            'value': '',
            'valuetype': 'string',
            'category': 'email',
            'description': 'Optional webhook URL alerts also POST to (e.g. a '
                           'Microsoft Teams Incoming Webhook or Workflow)'
        },
        {
            'key': 'alert_webhook_format',
            'value': 'teams',
            'valuetype': 'string',
            'category': 'email',
            'description': 'Alert webhook payload format: teams (classic '
                           'Incoming Webhook), adaptivecard (Teams Workflow), '
                           'or json (generic {title,text})'
        },
        # Audit log settings
        {
            'key': 'audit_retention_days',
            'value': '90',
            'valuetype': 'integer',
            'category': 'audit',
            'description': 'Number of days to retain audit logs (0 = keep forever)'
        },
        # Authentication settings
        {
            'key': 'saml_enabled',
            'value': 'false',
            'valuetype': 'boolean',
            'category': 'auth',
            'description': 'Enable SAML SSO authentication'
        },
        {
            'key': 'saml_idp_metadata_url',
            'value': '',
            'valuetype': 'string',
            'category': 'auth',
            'description': 'SAML Identity Provider metadata URL'
        },
        {
            'key': 'saml_entity_id',
            'value': '',
            'valuetype': 'string',
            'category': 'auth',
            'description': 'SAML Service Provider entity ID (e.g., https://shopdb.example.com)'
        },
        {
            'key': 'saml_acs_url',
            'value': '',
            'valuetype': 'string',
            'category': 'auth',
            'description': 'SAML Assertion Consumer Service URL'
        },
        {
            'key': 'saml_allow_local_login',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'auth',
            'description': 'Allow local username/password login when SAML is enabled'
        },
        {
            'key': 'saml_auto_create_users',
            'value': 'true',
            'valuetype': 'boolean',
            'category': 'auth',
            'description': 'Automatically create users on first SAML login'
        },
        {
            'key': 'saml_admin_group',
            'value': '',
            'valuetype': 'string',
            'category': 'auth',
            'description': 'SAML group name that grants admin role'
        },
    ]

    return defaults


@settings_bp.route('/seed', methods=['POST'])
@jwt_required()
@require_permission('settings.edit')
def seed_default_settings():
    """Seed default settings if they don't exist."""
    created = 0
    for d in build_default_settings():
        if not Setting.query.filter_by(key=d['key']).first():
            setting = Setting(**d)
            db.session.add(setting)
            created += 1

    db.session.commit()
    invalidate_settings_cache()

    return success_response({'created': created}, message=f'{created} default settings created')
