"""Settings API routes."""

import os

from flask import Blueprint, request, current_app, send_from_directory
from flask_jwt_extended import jwt_required
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

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'}


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

# 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 = '********'

# 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 = ['equipment', 'computer', 'printer', 'network_device']

# 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',
    'equipment': 'Equipment',
    'computer': 'PCs',
    'printer': 'Printers',
    'network_device': 'Network Devices',
    'notification': 'Notifications',
    'subnet': 'Subnets',
}

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_from_directory(_map_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()
    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)

    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). New keys default to a plugin-scoped string setting.
    if not setting:
        setting = Setting(key=key, value='', valuetype='string', category='plugin')
        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('', 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 the West
    # Jefferson sitemap so an un-reconfigured install still renders.
    mapdefaults = [
        {
            'key': 'map_blueprint_light',
            'value': '/static/images/sitemap2025-light.png',
            'valuetype': 'string',
            'category': 'map',
            'description': 'Floor-map blueprint image (light theme) for this facility'
        },
        {
            'key': 'map_blueprint_dark',
            'value': '/static/images/sitemap2025-dark.png',
            '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': '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': 'West Jefferson',
            'valuetype': 'string',
            'category': 'site',
            'description': 'Facility name shown on the shopfloor dashboard header'
        },
        {
            'key': 'pc_access_domain',
            'value': 'device.geaerospace.net',
            '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.'
        },
    ]

    # Collector pc-type -> ComputerType mapping is computers-plugin domain;
    # the plugin seeds pctypemap_<pxetype> settings on install.
    defaults = sitedefaults + identifierdefaults + searchdefaults + mapdefaults + [
        # 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)'
        },
        # 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')
