"""Dashboard defaults API: visitor-IP -> business-unit mapping.

CRUD for the mappings plus a resolve endpoint a kiosk/lobby dashboard calls to
auto-select its business unit from the display PC's IP.
"""

from flask import Blueprint, request
from flask_jwt_extended import jwt_required

from shopdb.extensions import db
from shopdb.core.models import DashboardDefault, BusinessUnit, AuditLog
from shopdb.core.models.dashboarddefault import (
    DISPLAY_ROLES, DISPLAY_ROLE_PATHS, normalize_display_role)
from shopdb.utils.responses import success_response, error_response, ErrorCodes
from shopdb.utils.authz import require_role

dashboarddefaults_bp = Blueprint('dashboarddefaults', __name__)


def _request_ip():
    """Caller IP for the IP fallback, port stripped so it matches a stored
    (portless) DashboardDefault.ipaddress. ARR forwards clientip:port."""
    from shopdb.utils.clientip import client_ip
    return client_ip(request)


def _serialize(default):
    data = default.to_dict()
    data['businessunit'] = default.businessunit.businessunit \
        if default.businessunit else None
    data['displaypath'] = default.displaypath
    return data


def _resolve_default(fqdn, ipaddress):
    """Find the active mapping for a display: FQDN first (stable), then IP."""
    default = None
    if fqdn:
        default = DashboardDefault.query.filter_by(
            fqdn=fqdn, isactive=True).first()
    if not default and ipaddress:
        default = DashboardDefault.query.filter_by(
            ipaddress=ipaddress, isactive=True).first()
    return default


@dashboarddefaults_bp.route('/visitor-location', methods=['GET'])
def visitor_location():
    """Resolve the business unit for the calling display by its IP.

    Unauthenticated: kiosks/lobby displays hit this. Returns the mapped
    business unit, or a null businessunitid when the IP is not mapped.
    """
    fqdn = (request.args.get('fqdn') or '').strip().lower() or None
    ipaddress = request.args.get('ipaddress') or _request_ip()
    default = _resolve_default(fqdn, ipaddress)
    if not default:
        return success_response({
            'fqdn': fqdn,
            'ipaddress': ipaddress,
            'businessunitid': None,
            'businessunit': None,
        })
    return success_response({
        'fqdn': fqdn,
        'ipaddress': ipaddress,
        'businessunitid': default.businessunitid,
        'businessunit': default.businessunit.businessunit
        if default.businessunit else None,
    })


@dashboarddefaults_bp.route('/display-role', methods=['GET'])
def display_role():
    """Resolve what a single 'display' PC should show, from its own IP.

    Unauthenticated: the display launcher calls this at boot. Returns the role
    (dashboard / lobby / partskiosk), the frontend path it maps to, and the
    business unit for the dashboard role. Unmapped IP -> null role.
    """
    fqdn = (request.args.get('fqdn') or '').strip().lower() or None
    ipaddress = request.args.get('ipaddress') or _request_ip()
    default = _resolve_default(fqdn, ipaddress)
    if not default:
        return success_response({
            'fqdn': fqdn,
            'ipaddress': ipaddress,
            'role': None,
            'path': None,
            'businessunitid': None,
            'businessunit': None,
        })
    return success_response({
        'fqdn': fqdn,
        'ipaddress': ipaddress,
        'role': default.displayrole,
        'path': default.displaypath,
        'businessunitid': default.businessunitid,
        'businessunit': default.businessunit.businessunit
        if default.businessunit else None,
    })


@dashboarddefaults_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_defaults():
    """List all visitor-IP -> business-unit mappings."""
    defaults = DashboardDefault.query.filter_by(isactive=True).order_by(
        DashboardDefault.ipaddress).all()
    return success_response([_serialize(d) for d in defaults])


@dashboarddefaults_bp.route('', methods=['POST'])
@jwt_required()
@require_role('admin')
def create_default():
    """Create a visitor-IP -> business-unit mapping."""
    data = request.get_json() or {}
    fqdn = (data.get('fqdn') or '').strip().lower() or None
    ipaddress = (data.get('ipaddress') or '').strip() or None
    # Accept any casing and the retired spellings, store the canonical one, so a
    # caller matching the kiosk's own file is never turned away over a capital.
    role = normalize_display_role(data.get('displayrole') or 'Dashboard')
    businessunitid = data.get('businessunitid')

    if not fqdn and not ipaddress:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              'fqdn or ipaddress is required')
    if not role:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
    # Only the Dashboard role needs a business unit (location).
    if role == 'Dashboard':
        if not businessunitid:
            return error_response(ErrorCodes.VALIDATION_ERROR,
                                  'a location is required for the dashboard role')
        if not db.session.get(BusinessUnit, businessunitid):
            return error_response(ErrorCodes.NOT_FOUND, 'Location not found',
                                  http_code=404)
    else:
        businessunitid = None
    if fqdn and DashboardDefault.query.filter_by(fqdn=fqdn, isactive=True).first():
        return error_response(ErrorCodes.CONFLICT,
                              f"{fqdn} is already mapped", http_code=409)
    if ipaddress and DashboardDefault.query.filter_by(
            ipaddress=ipaddress, isactive=True).first():
        return error_response(ErrorCodes.CONFLICT,
                              f"IP {ipaddress} is already mapped", http_code=409)

    default = DashboardDefault(fqdn=fqdn, ipaddress=ipaddress, displayrole=role,
                               businessunitid=businessunitid,
                               description=data.get('description'))
    db.session.add(default)
    db.session.flush()
    AuditLog.log('created', 'DashboardDefault', entityid=default.dashboarddefaultid,
                 entityname=(fqdn or ipaddress))
    db.session.commit()
    return success_response(_serialize(default), message='Mapping created',
                            http_code=201)


@dashboarddefaults_bp.route('/<int:default_id>', methods=['PUT'])
@jwt_required()
@require_role('admin')
def update_default(default_id):
    """Update a mapping."""
    default = db.session.get(DashboardDefault, default_id)
    if not default:
        return error_response(ErrorCodes.NOT_FOUND, 'Mapping not found', http_code=404)

    data = request.get_json() or {}
    if 'displayrole' in data:
        role = normalize_display_role(data.get('displayrole'))
        if not role:
            return error_response(ErrorCodes.VALIDATION_ERROR,
                                  f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
        default.displayrole = role
    if 'businessunitid' in data:
        if data['businessunitid'] and not db.session.get(BusinessUnit, data['businessunitid']):
            return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found',
                                  http_code=404)
        default.businessunitid = data['businessunitid']
    if 'fqdn' in data:
        default.fqdn = (data.get('fqdn') or '').strip().lower() or None
    if 'ipaddress' in data:
        default.ipaddress = (data.get('ipaddress') or '').strip() or None
    if 'description' in data:
        default.description = data['description']

    if not default.fqdn and not default.ipaddress:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              'fqdn or ipaddress is required')
    # Non-Dashboard roles carry no location; Dashboard needs one.
    if normalize_display_role(default.displayrole) != 'Dashboard':
        default.businessunitid = None
    elif not default.businessunitid:
        return error_response(ErrorCodes.VALIDATION_ERROR,
                              'a location is required for the dashboard role')

    db.session.commit()
    return success_response(_serialize(default), message='Mapping updated')


@dashboarddefaults_bp.route('/<int:default_id>', methods=['DELETE'])
@jwt_required()
@require_role('admin')
def delete_default(default_id):
    """Delete (deactivate) a mapping."""
    default = db.session.get(DashboardDefault, default_id)
    if not default:
        return error_response(ErrorCodes.NOT_FOUND, 'Mapping not found', http_code=404)
    default.isactive = False
    db.session.commit()
    return success_response(message='Mapping deleted')
