"""Reproduce bounded channel allocation; not a cable, thermal or cost model."""
import csv
import math
from collections import Counter
from pathlib import Path

HERE = Path(__file__).resolve().parent
SOURCE = HERE.parent / 'yacht-circuits' / 'circuit-inventory.csv'
records = list(csv.DictReader(SOURCE.open()))
assigned = [r for r in records if r['vessel'] != 'V04' and 'DC' in r['voltage']
            and r['path_class'] in ('load', 'control')]
essential_s358 = {'R1C1', 'R1C7', 'R3C1', 'R3C2', 'R3C3', 'R5C1', 'R5C2', 'R5C3', 'R5C5'}
helm_s358 = {'R1C10', 'R2C1', 'R2C2', 'R2C4', 'R2C8', 'R2C9', 'R3C8', 'R3C9',
             'R3C10', 'R3C11', 'R4C1', 'R4C2', 'R4C4', 'R4C5', 'R4C6', 'R5C6', 'R5C10', 'R5C11'}
cabin_s358 = {'R1C8', 'R1C9', 'R3C12', 'R4C3', 'R4C7', 'R4C9', 'R4C10'}
# Proposed dimming groups, not proof that installed lamps accept supply PWM.
# Navigation-mode lamps and a controller-fed spotlight retain steady supply.
pwm_groups = {
    'V01-helm-01', 'V01-helm-02', 'V01-helm-03', 'V01-helm-14',
    'V02-DC-D', 'V02-DC-E', 'V02-DC-F',
    'V03-DC-04', 'V03-DC-05', 'V03-DC-06', 'V03-DC-07',
    'V05-small-R2C3', 'V05-small-R2C4', 'V05-small-R2C8', 'V05-small-R2C9',
    'V05-small-R2C10', 'V05-small-R2C11', 'V05-small-R4C8', 'V05-small-R4C9', 'V05-small-R4C10',
    'V06-24FB-2F3', 'V06-salon-2S1', 'V06-salon-2S2', 'V06-salon-2S4', 'V06-salon-2S10',
    'V06-forward-2FW8', 'V06-forward-2FW9',
}
pwm_channels = {f'L{i:02}' for i in range(1, 7)} | {'H01', 'H02'}
allocation = []
for r in assigned:
    v, panel, ref = r['vessel'], r['panel'], r['reference']
    route, zone, reason = 'core', '', 'Retain local appliance controls; switch protected supply only'
    if float(r['protection_A']) > 30:
        route, reason = 'external', 'Retain protected heavy path; no direct Core power switching'
    elif v == 'V01' and (panel == 'source' or ref == '11'):
        route, reason = 'external', 'Retain stereo memory and both bilge paths without backfeed redesign'
    elif v == 'V05' and ref in essential_s358:
        route, reason = 'external', 'Conservative retained essential/monitor/pump group; not all are proven always-on'
    elif v == 'V06' and panel in ('essential', 'audio-fuse', '12FB'):
        route, reason = 'external', 'Retain essential source taps and low-demand 12 V audio distribution'
    if route == 'core':
        if v in ('V01', 'V02', 'V03'):
            zone = 'main'
        elif v == 'V05':
            zone = ('helm-bow' if ref in helm_s358 else
                    'cabin' if ref in cabin_s358 or ref in ('vacuflush', 'macerator') else 'cockpit-service')
        else:
            zone = panel
    allocation.append(dict(source_record=r['id'], vessel=v, pdf_page=r['pdf_page'],
                           function=r['function'], protection_A=r['protection_A'],
                           route=route, proposed_zone=zone, channel='', channel_limit_A='',
                           output_mode='pwm_lighting' if r['id'] in pwm_groups else 'on_off' if route == 'core' else 'retained_external',
                           channel_pwm_capable='', lamp_pwm_compatibility='unverified' if r['id'] in pwm_groups else 'not_applicable',
                           allocation_basis='design scenario; physical routing and source taps require survey', notes=reason))

zones = {}
for row in allocation:
    if row['route'] == 'core':
        zones.setdefault((row['vessel'], row['proposed_zone']), []).append(row)
reserves = {('V03', 'main'): 3, ('V06', 'salon'): 4, ('V06', 'forward'): 3}
zone_rows = []
for (v, zone), items in zones.items():
    reserve = reserves.get((v, zone), 2)
    assert len(items) + reserve <= 22
    # Allocate PWM lighting first; ordinary controls prefer switch-only positions.
    free = ([(f'L{i:02}', 15) for i in range(7, 17)] + [(f'H{i:02}', 30) for i in range(3, 7)]
            + [(f'L{i:02}', 15) for i in range(1, 7)] + [('H01', 30), ('H02', 30)])
    for item in sorted(items, key=lambda x: (x['output_mode'] != 'pwm_lighting', -float(x['protection_A']))):
        index = next(i for i, (name, cap) in enumerate(free)
                     if cap >= float(item['protection_A'])
                     and (item['output_mode'] != 'pwm_lighting' or name in pwm_channels))
        channel, cap = free.pop(index)
        item.update(channel=channel, channel_limit_A=cap, channel_pwm_capable='yes' if channel in pwm_channels else 'no')
    zone_rows.append(dict(vessel=v, zone=zone, assigned=len(items), above_15A=sum(float(x['protection_A']) > 15 for x in items),
                          required_spares=reserve, available_spares=len(free), node_count=1,
                          pwm_lighting=sum(x['output_mode'] == 'pwm_lighting' for x in items),
                          on_off=sum(x['output_mode'] == 'on_off' for x in items),
                          pwm_above_15A=sum(x['output_mode'] == 'pwm_lighting' and float(x['protection_A']) > 15 for x in items),
                          unallocated_pwm_slots=sum(name in pwm_channels for name, _ in free),
                          assigned_amps_sum_is_load_current='NO', simultaneous_load_qualified='NO'))

def write(name, rows):
    with (HERE / name).open('w', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys(), lineterminator="\n")
        writer.writeheader()
        writer.writerows(rows)

write('branch-allocation.csv', allocation)
write('zone-budget.csv', zone_rows)
comparisons = []
for label, sizes in [('uniform16', [16]), ('uniform22', [22]), ('uniform24', [24]),
                     ('uniform32', [32]), ('uniform46', [46]), ('mixed16_22', [16, 22])]:
    nodes = slots = high_slots = 0
    for z in zone_rows:
        required = z['assigned'] + z['required_spares']
        # Each hypothetical family reserves ceil(size/4) stages for 30 A; others are 15 A.
        # Not the published ratings of legacy S/M or any vendor product.
        alternatives = []
        for size in sizes:
            high = math.ceil(size / 4)
            count = max(math.ceil(required / size), math.ceil(z['above_15A'] / high))
            alternatives.append((count, count * size, count * high))
        n, s, h = min(alternatives)
        nodes += n; slots += s; high_slots += h
    used = sum(z['assigned'] for z in zone_rows)
    comparisons.append(dict(candidate=label, comparison_scope='count_and_current_only; PWM allocation verified for selected Core22 separately', power_designs=len(sizes), nodes=nodes, populated_output_positions=slots,
                            assigned_outputs=used, unused_positions_including_required_spares=slots-used,
                            high_current_positions=high_slots, utilization_percent=round(100*used/slots, 1)))
write('candidate-comparison.csv', comparisons)
assert len(allocation) == len({r['source_record'] for r in allocation}) == 151
assert len(zones) == 9
assert sum(r['route'] == 'core' for r in allocation) == 120
assert sum(r['route'] == 'external' for r in allocation) == 31
assert {r['source_record'] for r in allocation if r['output_mode'] == 'pwm_lighting'} == pwm_groups
assert all(r['channel_pwm_capable'] == 'yes' for r in allocation if r['output_mode'] == 'pwm_lighting')
assert sum(r['output_mode'] == 'pwm_lighting' for r in allocation) == 27
assert sum(r['output_mode'] == 'on_off' for r in allocation) == 93
assert max(z['pwm_lighting'] for z in zone_rows) == 4
assert max(z['pwm_above_15A'] for z in zone_rows) == 2
for v in ['V01', 'V02', 'V03', 'V05', 'V06']:
    print(v, dict(Counter(r['route'] for r in allocation if r['vessel'] == v)))
for z in zone_rows:
    print(z['vessel'], z['zone'], 'used', z['assigned'], 'high', z['above_15A'], 'spare', z['available_spares'])
for r in comparisons:
    print(r)
