"""THRESH-01: 표시 필터와 기존 downstream 필터를 분리한 최소 설정 projection."""
from copy import deepcopy
import hashlib
import json
import math
from app.intrusion import IntrusionConfig
from app.loitering import LoiteringConfig
from app.region_scenario import validate as validate_region_scenario
from app.class_catalog import COCO_NAMES
from app.pose_config import PoseConfig, execution_plan
from app.fire_config import FireConfig, FIRE_MODEL, FIRE_NAMES
from app.byte_tracking import TrackingSettings
from app.fall_runtime import FallSettings, validate as validate_fall
from app.fight_runtime import FightSettings, validate as validate_fight
from app.crowd_config import CrowdConfig, CROWD_MODEL, CROWD_NAMES


def tracking_settings(spec):
    validate_region_scenario(spec)
    validate_fall(spec)
    validate_fight(spec)
    tracking = TrackingSettings.parse(spec.get('tracking', {}))
    if not tracking.enabled:
        if not spec.get('server_id') or not spec.get('source_id'):
            raise ValueError('Canonical identity is required for Tracking Off.')
        if IntrusionConfig.parse(spec.get('intrusion', {})).enabled:
            raise ValueError('INTRUSION requires Tracking On.')
    return tracking


def display_categories(value):
    if value is None:
        return None
    if (type(value) is not list or len(value) > 80
            or any(type(k) is not str or k not in (*COCO_NAMES, *FIRE_NAMES, *CROWD_NAMES) for k in value)
            or len(set(value)) != len(value)):
        raise ValueError('Invalid display categories.')
    return sorted(value)


def visual_options(spec):
    value = spec.get('visualization') or {}
    if not isinstance(value, dict) or set(value) - {'score_threshold', 'categories', 'debug_all_candidates'}:
        raise ValueError('Invalid visualization settings.')
    categories = display_categories(value.get('categories'))
    debug = value.get('debug_all_candidates', False)
    if type(debug) is not bool:
        raise ValueError('Invalid debug candidate setting.')
    return categories, debug


def score(value):
    if type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1:
        raise ValueError('Score must be finite and between zero and one.')
    return value


def display_score(spec, fallback):
    value = spec.get('visualization') or {}
    visual_options(spec)
    return score(value.get('score_threshold', fallback))


def revision(spec):
    return hashlib.sha256(json.dumps(spec, sort_keys=True, separators=(',', ':'),
                                    allow_nan=False).encode()).hexdigest()


def settings_view(spec):
    downstream = ((spec.get('inference_profile') or {}).get('thresholds') or {}).get('score', .5)
    plan = execution_plan(spec)
    from app.provider_plan import plan as make_provider_plan
    providers = make_provider_plan(spec)
    categories = []
    for provider in providers.providers:
        if provider.key == 'fire':
            names = FIRE_NAMES
            for i, key in enumerate(names):
                categories.append(dict(key=key, displayName=key, modelKey=provider.model,
                                       classId=i, scenarios=['FIRE']))
        elif provider.key == 'crowd':
            for i, key in enumerate(CROWD_NAMES):
                categories.append(dict(key=key, displayName=key, modelKey=provider.model,
                                       classId=i, scenarios=['CROWD']))
        elif provider.key == 'pose':
            categories.append(dict(key='person', displayName='person', modelKey=provider.model,
                                   classId=0, scenarios=list(provider.scenarios)))
        else:
            for i, key in enumerate(COCO_NAMES):
                categories.append(dict(
                    key=key, displayName=key, modelKey=provider.model, classId=i,
                    scenarios=(list(provider.scenarios) if i == 0 else [])))
    return dict(
        revision=revision(spec), display_score=display_score(spec, downstream),
        pose=PoseConfig.parse(spec.get('pose', {})).payload(),
        fire=FireConfig.parse(spec.get('fire', {})).payload(),
        execution_plan=plan, downstream_score=downstream,
        frame_time_anchor=spec.get('frame_time_anchor'),
        analysisFpsTarget=spec.get('analysis_fps_target', 10),
        tracking=tracking_settings(spec).payload(),
        intrusion=IntrusionConfig.parse(spec.get('intrusion', {})).payload(),
        loitering=LoiteringConfig.parse(spec.get('loitering', {})).payload(),
        fall=FallSettings.parse(spec.get('fall') or {}).settings_payload(),
        fight=FightSettings.parse(spec.get('fight') or {}).settings_payload(),
        crowd=CrowdConfig.parse(spec.get('crowd') or {}).settings_payload(),
        display_categories=visual_options(spec)[0],
        debug_all_candidates=visual_options(spec)[1],
        categories=categories)


def patch_settings(spec, changes):
    if not changes or set(changes) - {'tracking', 'fire', 'fall', 'fight', 'crowd', 'pose', 'display_score', 'downstream_score', 'frame_time_anchor', 'analysisFpsTarget', 'intrusion', 'loitering', 'display_categories', 'debug_all_candidates'}:
        raise ValueError('Unsupported stream settings.')
    candidate = deepcopy(spec)
    for field, value in changes.items():
        if field in ('fight', 'crowd'):
            if type(value) is not dict:
                raise ValueError('Invalid scenario patch.')
            parser = FightSettings if field == 'fight' else CrowdConfig
            current = parser.parse(spec.get(field) or {}).settings_payload()
            merged=dict(current, **value)
            if field=='fight' and value.get('engine')=='legacy':merged.pop('r5',None)
            configured = parser.parse(merged).settings_payload()
            if configured != current:
                candidate[field] = configured
            continue
        if field == 'fall':
            if type(value) is not dict:
                raise ValueError('Invalid FALL settings patch.')
            current = FallSettings.parse(spec.get('fall') or {}).settings_payload()
            configured = FallSettings.parse(dict(current, **value)).settings_payload()
            if configured != current:
                candidate[field] = configured
            continue
        if field == 'tracking':
            if type(value) is not dict:
                raise ValueError('Invalid tracking settings.')
            current = TrackingSettings.parse(spec.get('tracking', {})).payload()
            configured = TrackingSettings.parse(dict(current, **value))
            if configured.payload() != current:
                candidate['tracking'] = dict(spec.get('tracking', {}), **value)
            continue
        if field == 'fire':
            if type(value) is not dict:
                raise ValueError('Invalid Fire settings.')
            current = FireConfig.parse(spec.get('fire', {})).payload()
            configured = FireConfig.parse(current).patched(value)
            if configured.payload() != current:
                candidate['fire'] = configured.payload()
            continue
        if field == 'pose':
            if not isinstance(value, dict):
                raise ValueError('Invalid Pose settings.')
            current = PoseConfig.parse(spec.get('pose', {})).payload()
            configured = PoseConfig.parse(dict(current, **value))
            if configured.payload() != current:
                candidate['pose'] = configured.payload()
            continue
        if field in ('display_categories', 'debug_all_candidates'):
            if not spec.get('server_id') or not spec.get('source_id'):
                raise ValueError('Canonical identity is required for display settings.')
            if field == 'display_categories':
                value = display_categories(value)
                index, key = 0, 'categories'
            else:
                if type(value) is not bool:
                    raise ValueError('Invalid debug candidate setting.')
                index, key = 1, field
            if value != visual_options(spec)[index]:
                candidate['visualization'] = dict(candidate.get('visualization') or {})
                candidate['visualization'][key] = value
            continue
        if field == 'loitering':
            if type(value) is not dict:
                raise ValueError('Invalid LOITERING settings.')
            current = LoiteringConfig.parse(spec.get('loitering', {})).payload()
            configured = LoiteringConfig.parse(dict(current, **value))
            if configured.payload() != current:
                candidate[field] = configured.payload()
            continue
        if field == 'intrusion':
            if not isinstance(value, dict):
                raise ValueError('Invalid INTRUSION settings.')
            current = IntrusionConfig.parse(spec.get('intrusion', {})).payload()
            configured = IntrusionConfig.parse(dict(current, **value))
            if configured.enabled and (not spec.get('server_id') or not spec.get('source_id')):
                raise ValueError('Canonical identity is required for INTRUSION.')
            if configured.payload() != current:
                candidate[field] = configured.payload()
            continue
        if field == 'analysisFpsTarget':
            from app.analysis_metrics import analysis_fps
            if not spec.get('server_id') or not spec.get('source_id'):
                raise ValueError('Canonical Frame identity is required for FPS selection.')
            value = analysis_fps(value)
            if value != spec.get('analysis_fps_target', 10):
                candidate['analysis_fps_target'] = value
            continue
        if field == 'frame_time_anchor':
            from app.frame_time import FrameAnchor
            if value is not None and (not spec.get('server_id') or not spec.get('source_id')):
                raise ValueError('Canonical Frame identity is required for time settings.')
            candidate[field] = FrameAnchor.model_validate(value).model_dump() if value is not None else None
            continue
        score(value)
        if field == 'display_score':
            if not spec.get('server_id') or not spec.get('source_id'):
                raise ValueError('Canonical Frame identity is required for display settings.')
            candidate['visualization'] = dict(candidate.get('visualization') or {})
            candidate['visualization']['score_threshold'] = value
        else:
            if value == 0:
                raise ValueError('Downstream score must be greater than zero.')
            profile = candidate['inference_profile'] = dict(candidate.get('inference_profile') or {})
            profile['thresholds'] = dict(profile.get('thresholds') or {})
            profile['thresholds']['score'] = value
    execution_plan(candidate)
    tracking_settings(candidate)
    return candidate
