"""Scenario-local Web tuning projection for PR13.

Public Start parameters are permissive by design: unknown/null/invalid tuning
values are ignored, while the surrounding Start/ROI/identity contract remains
strict in secure_control.  A candidate scenario config is parsed before it is
stored so invalid inter-field combinations fall back atomically.
"""
from __future__ import annotations

from copy import deepcopy
import logging
import math
import re
from app.crowd_config import EVENT_PARAMETERS

LOG = logging.getLogger(__name__)


def _number(value, *, low=None, high=None, low_open=False):
    if type(value) not in (int, float):
        return None
    try:
        if not math.isfinite(value):
            return None
    except OverflowError:
        return None
    value = float(value)
    if low is not None and (value <= low if low_open else value < low):
        return None
    if high is not None and value > high:
        return None
    return value


def _integer(value, *, low, high):
    return value if type(value) is int and low <= value <= high else None


_RULES = {
    'INTRUSION': {
        'person_confidence': lambda v: _number(v, low=0, high=1),
        'dwell_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'rearm_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'grace_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
    },
    'LOITERING': {
        'person_confidence': lambda v: _number(v, low=0, high=1),
        'dwell_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'rearm_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'grace_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
    },
    'FALL': {
        'person_confidence': lambda v: _number(v, low=0, high=1),
        'pose_confidence': lambda v: _number(v, low=0, high=1),
        'down_angle_deg': lambda v: _number(v, low=0, high=90),
        'recovery_hold_seconds': lambda v: _number(v, low=.001, high=3600),
    },
    'FIGHT': {
        'person_confidence': lambda v: _number(v, low=0, high=1),
        'pose_confidence': lambda v: _number(v, low=0, high=1),
        'pair_distance_ratio': lambda v: _number(v, low=0, high=30, low_open=True),
        'pair_close_hold_seconds': lambda v: _number(v, low=0, high=30, low_open=True),
        'arm_motion_ratio': lambda v: _number(v, low=0, high=30, low_open=True),
        'required_bursts': lambda v: _integer(v, low=1, high=16),
    },
    'CROWD': {
        'person_confidence': lambda v: _number(v, low=0, high=1),
        'head_confidence': lambda v: _number(v, low=0, high=1),
        'window_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'report_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
    },
    'FIRE': {
        'flame_confidence': lambda v: _number(v, low=0, high=1),
        'flame_window_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'flame_positive_ratio': lambda v: _number(v, low=0, high=1, low_open=True),
        'smoke_confidence': lambda v: _number(v, low=0, high=1),
        'smoke_window_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
        'smoke_positive_ratio': lambda v: _number(v, low=0, high=1, low_open=True),
        'clear_seconds': lambda v: _number(v, low=0, high=3600, low_open=True),
    },
}


def sanitize(code, raw):
    """Return valid public tuning values; invalid/unknown values are ignored."""
    if raw is None:
        return {}, ()
    if type(raw) is not dict:
        return {}, (('*', 'INVALID_CONTAINER'),)
    rules = _RULES[code]
    accepted, ignored = {}, []
    for key, value in raw.items():
        if key == 'cooldown_seconds' or (code == 'CROWD' and key in EVENT_PARAMETERS):
            continue  # Admission settings are validated separately from optional tuning.
        rule = rules.get(key)
        if rule is None:
            ignored.append((str(key), 'UNKNOWN_PARAMETER'))
            continue
        if value is None:
            ignored.append((key, 'NULL_PARAMETER'))
            continue
        parsed = rule(value)
        if parsed is None:
            ignored.append((key, 'INVALID_VALUE'))
            continue
        accepted[key] = parsed
    return accepted, tuple(ignored)


def _log_ignored(code, ignored):
    for key, reason in ignored[:32]:
        key = key if re.fullmatch(r'[A-Za-z0-9_]{1,64}', key) else 'INVALID_KEY'
        LOG.info('scenario_parameter_ignored scenario=%s parameter=%s reason=%s',
                 code, key, reason)


def apply(spec, request):
    """Atomically apply valid tuning values to one private candidate spec.

    Returns True when the override set was accepted.  False means either no
    usable values were supplied or the complete candidate failed scenario
    validation; in both cases the original saved/default config is preserved.
    """
    code = request['scenarioCode']
    if code == 'FALL':
        from app.fall_r2.web import apply_parameters
        handled = apply_parameters(spec, request)
        if handled is not None:
            return handled
    if code == 'FIGHT':
        from app.fight_r5.web import apply_parameters
        handled = apply_parameters(spec, request)
        if handled is not None:
            return handled
    from app.event_cooldown import default_seconds
    raw = request.get('parameters')
    cooldown = _number(raw.get('cooldown_seconds'), low=0, high=300) if type(raw) is dict else None
    spec.setdefault('event_cooldowns', {})[code] = default_seconds() if cooldown is None else cooldown
    if code == 'CROWD':
        from app.crowd_config import CrowdConfig, validate_event_parameters
        spec.pop('crowd_start_rejection', None)
        current = CrowdConfig.parse(spec.get('crowd') or {})
        if request['enabled']:
            try:
                required = validate_event_parameters(raw)
            except ValueError as error:
                reason = str(error)
                spec['crowd'] = current.patched(dict(enabled=False, event_enabled=False)).settings_payload()
                spec['crowd_start_rejection'] = reason
                LOG.info('scenario_skipped scenario=CROWD reason=%s', reason)
                return False
            spec['crowd'] = current.patched(dict(required, event_enabled=True)).settings_payload()
        else:
            spec['crowd'] = current.patched(dict(event_enabled=False)).settings_payload()
    values, ignored = sanitize(code, request.get('parameters'))
    _log_ignored(code, ignored)
    if not values:
        return cooldown is not None or (code == 'CROWD' and request['enabled'])

    before = deepcopy(spec)
    try:
        if code == 'INTRUSION':
            from app.intrusion import IntrusionConfig
            cfg = dict(spec.get('intrusion') or {})
            mapping = {'person_confidence': 'confidence'}
            cfg.update({mapping.get(k, k): v for k, v in values.items()})
            spec['intrusion'] = IntrusionConfig.parse(cfg).payload()

        elif code == 'LOITERING':
            from app.loitering import LoiteringConfig
            cfg = dict(spec.get('loitering') or {})
            mapping = {'person_confidence': 'confidence'}
            cfg.update({mapping.get(k, k): v for k, v in values.items()})
            spec['loitering'] = LoiteringConfig.parse(cfg).payload()

        elif code == 'FALL':
            from app.fall_runtime import FallSettings
            cfg = dict(spec.get('fall') or {})
            mapping = {'person_confidence': 'person_score', 'pose_confidence': 'joint_score'}
            cfg.update({mapping.get(k, k): v for k, v in values.items()})
            spec['fall'] = FallSettings.parse(cfg).settings_payload()

        elif code == 'FIGHT':
            from app.fight_runtime import FightSettings
            cfg = dict(spec.get('fight') or {})
            mapping = {'person_confidence': 'person_score', 'pose_confidence': 'joint_score'}
            cfg.update({mapping.get(k, k): v for k, v in values.items()})
            spec['fight'] = FightSettings.parse(cfg).settings_payload()

        elif code == 'CROWD':
            from app.crowd_config import CrowdConfig
            cfg = CrowdConfig.parse(spec.get('crowd') or {}).patched(values)
            spec['crowd'] = cfg.settings_payload()

        elif code == 'FIRE':
            from app.fire_config import FireConfig
            from app.fire_scenario import FireScenarioConfig
            current = FireConfig.parse(spec.get('fire') or {})
            cfg = (current.scenario.settings_payload() if current.scenario
                   else FireScenarioConfig().settings_payload())
            cfg.update(values)
            scenario = FireScenarioConfig.parse(cfg)
            spec['fire'] = FireConfig(enabled=current.enabled, scenario=scenario).payload()
        else:
            raise ValueError('Unknown scenario code.')
        return True
    except (TypeError, ValueError):
        spec.clear()
        spec.update(before)
        LOG.info('scenario_parameter_override_reverted scenario=%s reason=INTER_FIELD_INVALID', code)
        return False


def legacy_projection_view(spec, scenarios):
    """Internal compatibility view for old projectors; never a public schema."""
    out = deepcopy(scenarios)
    for request in out:
        code = request['scenarioCode']
        if code == 'CROWD' and spec.get('crowd_start_rejection'):
            request['enabled'] = False
        if code == 'INTRUSION':
            from app.intrusion import IntrusionConfig
            request['confidence'] = IntrusionConfig.parse(spec.get('intrusion') or {}).confidence
        elif code == 'LOITERING':
            from app.loitering import LoiteringConfig
            request['confidence'] = LoiteringConfig.parse(spec.get('loitering') or {}).confidence
        elif code == 'FALL':
            from app.fall_runtime import FallSettings
            request['confidence'] = FallSettings.parse(spec.get('fall') or {}).policy.person_score
        elif code == 'FIGHT':
            from app.fight_runtime import FightSettings
            request['confidence'] = FightSettings.parse(spec.get('fight') or {}).policy.person_score
        else:
            request['confidence'] = None
    return out
