"""Explicit internal fixture adapter. Replace this boundary after Web schema agreement."""
from app.intrusion import IntrusionConfig
from app.multi_roi.errors import RoiError
from app.multi_roi.model import Area, Rule, Snapshot, FULL_FRAME, CONTRACT, canonical
from app.multi_roi.parameters import compile_settings


def identifier(v, *, zero=False):
    return type(v) is int and (0 if zero else 1) <= v <= 2**53-1


def normalize(value, policy):
    from app.secure_control import CODES, validate_start, ControlError
    if type(value) is not dict or value.get('contract') != CONTRACT:
        raise RoiError('CONTRACT_MODE_MISMATCH')
    required = {'contract', 'cameraId', 'rtspUrl', 'startWallTimeMs', 'rtpClockRate', 'rois', 'rules'}
    if not required <= set(value) or set(value)-required-{'recServerId', 'recCameraId'}:
        raise RoiError('INVALID_SNAPSHOT')
    source = {k: v for k, v in value.items() if k not in ('contract', 'rois', 'rules')}
    try:
        validate_start(dict(source, scenarios=[dict(scenarioCode=c, enabled=False) for c in sorted(CODES)]))
    except ControlError as exc:
        field = {'RTSP_INVALID':'rtspUrl', 'ANCHOR_INVALID':'startWallTimeMs',
                 'CLOCK_RATE_UNSUPPORTED':'rtpClockRate', 'CAMERA_ID_INVALID':'cameraId'}.get(str(exc), 'source')
        raise RoiError('INVALID_SNAPSHOT', field=field) from None
    camera = value['cameraId']
    if type(value['rois']) is not list or type(value['rules']) is not list:
        raise RoiError('INVALID_SNAPSHOT', camera=camera)
    if len(value['rois']) > policy['max_rois']:
        raise RoiError('ROI_LIMIT_EXCEEDED', camera=camera, actual=len(value['rois']), limit=policy['max_rois'])
    if len(value['rules']) > policy['max_rules']:
        raise RoiError('RULE_LIMIT_EXCEEDED', camera=camera, actual=len(value['rules']), limit=policy['max_rules'])
    areas = {}
    reserved = policy['full_frame_roi_id']
    for row in value['rois']:
        if type(row) is not dict or not {'roiId'} <= set(row) or set(row)-{'roiId','points','outside','label'}:
            raise RoiError('INVALID_ROI', camera=camera)
        rid = row['roiId']
        if not identifier(rid, zero=True) or (rid == 0 and reserved != 0):
            raise RoiError('INVALID_ROI', camera=camera)
        if rid in areas:
            raise RoiError('DUPLICATE_ROI_ID', camera=camera, roi=rid)
        outside = row.get('outside', False)
        if type(outside) is not bool:
            raise RoiError('INVALID_ROI', camera=camera, roi=rid, field='outside')
        try:
            points = row.get('points', FULL_FRAME if rid == reserved else None)
            cfg = IntrusionConfig(roi=points, roi_name=row.get('label', 'Full frame' if rid == reserved else 'ROI'))
        except (ValueError, TypeError, OverflowError):
            raise RoiError('INVALID_ROI', camera=camera, roi=rid) from None
        edges = zip(cfg.roi, cfg.roi[1:]+cfg.roi[:1])
        full = abs(abs(sum(a[0]*b[1]-b[0]*a[1] for a,b in edges))/2-1) <= 1e-12
        if rid == reserved and not full:
            raise RoiError('FULL_FRAME_ID_CONFLICT', camera=camera, roi=rid)
        if outside and full:
            raise RoiError('EMPTY_EFFECTIVE_REGION', camera=camera, roi=rid)
        points = tuple(tuple(float(v) for v in p) for p in cfg.roi)
        orientations = (points, tuple(reversed(points)))
        points = min(p[i:]+p[:i] for p in orientations for i in range(len(p)))
        areas[rid] = Area(rid, points, outside, cfg.roi_name, 'full_frame' if full else 'polygon', rid == reserved)
    rules, seen = [], set()
    for row in value['rules']:
        if (type(row) is not dict or not {'ruleId','roiId','scenarioCode'} <= set(row)
                or set(row)-{'ruleId','roiId','scenarioCode','enabled','parameters'}):
            raise RoiError('INVALID_RULE_CONFIG', camera=camera)
        uid, rid, code = row['ruleId'], row['roiId'], row['scenarioCode']
        if not identifier(uid, zero=True) or not identifier(rid, zero=True):
            raise RoiError('INVALID_RULE_CONFIG', camera=camera, field='identity')
        if uid in seen:
            raise RoiError('DUPLICATE_RULE_ID', camera=camera, rule=uid)
        seen.add(uid)
        if rid == reserved and rid not in areas:
            areas[rid] = Area(rid, FULL_FRAME, label='Full frame', kind='full_frame', reserved=True)
        if rid not in areas:
            raise RoiError('ROI_REFERENCE_NOT_FOUND', camera=camera, roi=rid, rule=uid)
        enabled = row.get('enabled', True)
        if type(code) is not str or code not in CODES or type(enabled) is not bool:
            raise RoiError('INVALID_RULE_CONFIG', camera=camera, rule=uid)
        try:
            settings, cooldown = compile_settings(code, row.get('parameters', {}), areas[rid], enabled)
        except RoiError as exc:
            exc.details.update(cameraId=camera, roiId=rid, ruleId=uid)
            raise
        rules.append(Rule(uid, rid, code, enabled, canonical(settings), cooldown))
    if len(areas) > policy['max_rois']:
        raise RoiError('ROI_LIMIT_EXCEEDED', camera=camera, actual=len(areas), limit=policy['max_rois'])
    return Snapshot(camera, canonical(source), tuple(areas[k] for k in sorted(areas)),
                    tuple(sorted(rules, key=lambda r: r.rule_id)))
