"""
AP700 write-only PyScript using older APK default calibration rows.

What this version does:
- keeps the known-good AP700 TCP framing/checksum
- uses SET_ALL_CHANNEL_INDEX only
- uses the older APK's built-in default calibration rows (0..53) for named colors
- computes channels from angle using the app-style color-region table
- uses user-facing intensity 0..100
- does NO live reads

Install:
  Save as /config/pyscript/ap700_default_cal.py
  Reload PyScript

Services:
  pyscript.ap700_on
  pyscript.ap700_off
  pyscript.ap700_power
  pyscript.ap700_find_me
  pyscript.ap700_send_hex
  pyscript.ap700_set_channels12
  pyscript.ap700_set_calibration_row
  pyscript.ap700_set_angle
  pyscript.ap700_set_color_name
  pyscript.ap700_color_names
"""

import socket

LIGHT_HOSTS = ["10.50.52.2", "10.50.52.3"]
PORT = 8899
TIMEOUT = 0.7

START_BYTE = 0xA9
STOP_BYTE = 0x5C

CMD_SET_ALL_CHANNEL_INDEX = 0x01
CMD_POWER = 0x27
CMD_FIND_ME = 0x76

# Older APK built-in default calibration rows 0..53.
CALIBRATION = [
    [720,3028,720,4095,4095,0,670,3632,670,3913,3913,0],
    [0,2318,0,2060,2060,0,0,2250,0,2000,2000,0],
    [0,807,0,0,1421,0,0,800,0,0,1400,0],
    [1100,2813,1100,4095,4095,0,1084,2746,1084,4034,4034,0],
    [821,2144,821,1870,2082,0,847,2118,847,1857,2056,0],
    [0,1255,731,794,786,0,0,1236,721,772,773,0],
    [1660,1704,1660,4095,4095,0,1626,1743,1626,4095,4095,0],
    [1071,1953,1164,1829,1878,0,1071,1953,1164,1829,1878,0],
    [724,1212,724,759,805,0,0,1178,875,751,746,0],
    [2223,1000,2223,3568,3508,0,2200,1635,2200,4095,2734,0],
    [1405,1684,1405,1720,1684,0,1405,1684,1405,1720,1684,0],
    [788,986,788,0,986,0,786,961,786,0,961,0],
    [2684,703,2684,3486,2955,0,2695,985,2695,3181,2734,0],
    [1676,1676,1676,1390,1502,0,1654,1698,1654,1401,1524,0],
    [832,913,832,0,913,0,826,903,826,0,898,0],
    [3161,770,3161,4054,1833,0,3333,1915,3333,2069,1967,0],
    [1918,1429,1918,1259,1314,0,1922,1451,1922,1257,1323,0],
    [885,857,885,0,857,0,884,852,884,0,842,0],
    [4015,1125,4015,2236,1466,0,4095,1466,4095,1677,1466,0],
    [2140,1240,2140,1080,1125,0,2144,1262,2144,1078,1134,0],
    [916,800,916,0,789,0,938,798,938,0,777,0],
    [3975,1129,3975,1200,1309,0,4095,1000,4095,1034,1599,0],
    [2200,1025,2200,853,1013,0,2320,1080,2345,828,1030,0],
    [972,911,972,0,0,0,974,909,974,0,0,0],
    [3975,880,3975,1020,1160,0,4095,900,4095,1000,1100,0],
    [2226,906,2226,779,881,0,2352,876,2352,787,850,0],
    [992,836,992,0,0,0,1004,824,1004,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,1785],
    [0,0,0,0,0,0,0,0,0,0,0,1298],
    [0,0,0,0,0,0,0,0,0,0,0,743],
    [0,860,0,880,860,0,0,850,0,950,850,0],
    [0,750,0,750,750,0,0,739,0,739,739,0],
    [0,0,0,0,754,0,0,0,0,0,700,0],
    [0,0,0,0,0,0,0,0,0,0,0,4095],
    [0,0,0,0,0,0,0,0,0,0,0,2169],
    [0,0,0,0,0,0,0,0,0,0,0,1021],
    [0,0,0,0,0,1520,0,0,0,0,0,3975],
    [0,0,0,0,0,970,0,0,568,0,0,2196],
    [0,0,0,0,0,726,0,0,0,0,0,1265],
    [0,0,0,0,0,4095,0,0,0,0,0,2760],
    [0,0,0,0,0,1797,0,0,0,0,0,1773],
    [0,0,0,0,0,795,0,0,0,0,0,837],
    [0,0,0,0,0,3200,0,0,0,0,0,0],
    [0,0,0,0,0,1772,0,0,0,0,0,0],
    [0,0,0,0,0,771,0,0,0,0,0,0],
    [1960,0,1960,2560,0,4095,2100,0,2100,2800,0,0],
    [1140,0,1010,2537,0,2594,1122,0,1122,1966,0,0],
    [0,0,690,1339,0,1315,0,0,694,1442,0,0],
    [1527,3817,1527,2005,3536,0,1476,3947,1476,2069,3654,858],
    [1018,2655,1018,1367,2331,0,1018,2628,1018,1346,2315,0],
    [0,924,0,1144,1261,0,0,873,0,1090,1202,987],
    [700,1460,740,690,710,0,710,1490,750,705,740,4075],
    [0,0,0,809,1110,0,0,0,0,894,1143,2315],
    [0,0,0,0,803,0,0,0,0,0,783,1043],
]

COLOR_REGIONS = {
    "sky blue": {"name": "Sky Blue", "high": 24, "mid": 25, "low": 26, "high_angle": 91.0, "low_angle": 109.0},
    "ocean blue 7": {"name": "Ocean Blue 7", "high": 21, "mid": 22, "low": 23, "high_angle": 110.0, "low_angle": 129.0},
    "ocean blue 6": {"name": "Ocean Blue 6", "high": 18, "mid": 19, "low": 20, "high_angle": 130.0, "low_angle": 149.0},
    "ocean blue 5": {"name": "Ocean Blue 5", "high": 15, "mid": 16, "low": 17, "high_angle": 150.0, "low_angle": 169.0},
    "ocean blue 4": {"name": "Ocean Blue 4", "high": 12, "mid": 13, "low": 14, "high_angle": 170.0, "low_angle": 189.0},
    "ocean blue 3": {"name": "Ocean Blue 3", "high": 9, "mid": 10, "low": 11, "high_angle": 190.0, "low_angle": 209.0},
    "ocean blue 2": {"name": "Ocean Blue 2", "high": 6, "mid": 7, "low": 8, "high_angle": 210.0, "low_angle": 229.0},
    "ocean blue 1": {"name": "Ocean Blue 1", "high": 3, "mid": 4, "low": 5, "high_angle": 230.0, "low_angle": 249.0},
    "deep ocean blue": {"name": "Deep Ocean Blue", "high": 0, "mid": 1, "low": 2, "high_angle": 250.0, "low_angle": 267.0},
    "indigo": {"name": "Indigo", "high": 48, "mid": 49, "low": 50, "high_angle": 270.0, "low_angle": 271.0},
    "purple": {"name": "Purple", "high": 51, "mid": 52, "low": 53, "high_angle": 285.0, "low_angle": 325.0},
    "red": {"name": "Red", "high": 33, "mid": 34, "low": 35, "high_angle": 325.0, "low_angle": 339.0},
    "orange": {"name": "Orange", "high": 36, "mid": 37, "low": 38, "high_angle": 340.0, "low_angle": 359.0},
    "yellow": {"name": "Yellow", "high": 39, "mid": 40, "low": 41, "high_angle": 15.0, "low_angle": 64.0},
    "green": {"name": "Green", "high": 42, "mid": 43, "low": 44, "high_angle": 65.0, "low_angle": 110.0},
    "blue": {"name": "Blue", "high": 45, "mid": 46, "low": 47, "high_angle": 90.0, "low_angle": 90.0},
    "moon red": {"name": "Moon Red", "high": 27, "mid": 28, "low": 29, "high_angle": 400.0, "low_angle": 400.0},
    "mood blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
    "moon blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
}


INTENSITY_MAPPING_PERCENT = [0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 18, 18, 18, 19, 19, 20, 20, 20, 21, 21, 22, 22, 22, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29, 30, 30, 31, 31, 31, 32, 32, 33, 33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 38, 39, 39, 40, 40, 40, 41, 41, 42, 42, 42, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 47, 47, 47, 48, 48, 49, 49, 49, 50, 50, 51, 51, 51, 52, 52, 53, 53, 53, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 58, 58, 58, 59, 59, 60, 60, 60, 61, 61, 62, 62, 62, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 67, 67, 67, 68, 68, 69, 69, 69, 70, 70, 71, 71, 71, 72, 72, 73, 73, 73, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 78, 78, 78, 79, 79, 80, 80, 80, 81, 81, 82, 82, 82, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 87, 87, 87, 88, 88, 89, 89, 89, 90, 90, 91, 91, 91, 92, 92, 93, 93, 93, 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 98, 98, 98, 99, 99, 100, 100]

ALIASES = {
    "cyan": "sky blue",
    "deep blue": "deep ocean blue",
    "ocean1": "ocean blue 1",
    "ocean2": "ocean blue 2",
    "ocean3": "ocean blue 3",
    "ocean4": "ocean blue 4",
    "ocean5": "ocean blue 5",
    "ocean6": "ocean blue 6",
    "ocean7": "ocean blue 7",
}

def _log_error(msg):
    try:
        log.error(msg)
    except Exception:
        print(msg)

def _normalize_name(name):
    key = str(name).strip().lower().replace("_", " ").replace("-", " ")
    return ALIASES.get(" ".join(key.split()), " ".join(key.split()))

def _clamp(v, lo=0, hi=255):
    v = int(v)
    if v < lo:
        return lo
    if v > hi:
        return hi
    return v

def _clamp_u16(v):
    return _clamp(v, 0, 4095)

def _pct_to_device_byte(percent):
    target = _clamp(percent, 0, 100)
    for i, pct in enumerate(INTENSITY_MAPPING_PERCENT):
        if pct >= target:
            return i
    return 255

def _checksum(data):
    total = 0
    for b in data:
        total = (total + int(b)) & 0xFF
    return (256 - total) & 0xFF

def _frame(body):
    msg = [len(body) + 1] + [int(b) & 0xFF for b in body]
    chk = _checksum(msg)
    return bytes([START_BYTE] + msg + [chk, STOP_BYTE])

def _target_hosts(host=None):
    if host is None or host == "":
        return list(LIGHT_HOSTS)
    if isinstance(host, str):
        return [host]
    return list(host)

def _u16le(v):
    v = _clamp_u16(v)
    return [v & 0xFF, (v >> 8) & 0xFF]

@pyscript_executor
def _send(host, payload, timeout=TIMEOUT):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.settimeout(timeout)
        sock.connect((host, PORT))
        sock.sendall(payload)
        return True
    finally:
        try:
            sock.close()
        except Exception:
            pass

def _send_all(payload, host=None, timeout=TIMEOUT):
    results = {}
    for one_host in _target_hosts(host):
        try:
            _send(one_host, payload, timeout=timeout)
            results[one_host] = True
        except Exception as e:
            results[one_host] = False
            _log_error("AP700 %s failed: %s" % (one_host, e))
    return results

def _cmd_power(state):
    return _frame([0x01, 0x00, CMD_POWER, _clamp(state, 0, 1)])

def _cmd_find_me():
    return _frame([0x01, 0x00, CMD_FIND_ME])

def _cmd_set_all_channel_index(channels12, apply_flag=1):
    vals = list(channels12[:12])
    while len(vals) < 12:
        vals.append(1)
    body = [0x01, 0x00, CMD_SET_ALL_CHANNEL_INDEX]
    for v in vals:
        body.extend(_u16le(v))
    body.extend([0, 0, 0, 0, 0, _clamp(apply_flag, 0, 255)])
    return _frame(body)

def _check_calibration_data650(channels12):
    out = list(channels12)
    if out[0] < 650 or out[6] < 650:
        out[0] = 1
        out[6] = 1
    if out[1] < 650 or out[7] < 650:
        out[1] = 1
        out[7] = 1
    if out[2] < 650 or out[8] < 650:
        out[2] = 1
        out[8] = 1
    if out[3] < 650 or out[9] < 650:
        out[3] = 1
        out[9] = 1
    return out

def _get_touch_channel_index(intensity255, region_name):
    key = _normalize_name(region_name)
    region = COLOR_REGIONS[key]
    d3 = float(_clamp(intensity255, 0, 255)) / 255.0
    if d3 == 1.0:
        return list(CALIBRATION[region["high"]])
    if 0.5 < d3 < 1.0:
        high = CALIBRATION[region["high"]]
        mid = CALIBRATION[region["mid"]]
        out = []
        for i in range(12):
            out.append(int(round(((high[i] - mid[i]) / 50.0) * (d3 - 0.5) * 100.0)) + mid[i])
        return out
    if 0.0 < d3 < 0.5:
        mid = CALIBRATION[region["mid"]]
        low = CALIBRATION[region["low"]]
        out = []
        for i in range(12):
            out.append(int(round(((mid[i] - low[i]) / 50.0) * d3 * 100.0)) + low[i])
        return out
    if d3 == 0.0:
        return [1] * 12
    return list(CALIBRATION[region["mid"]])

def _get_touch_channel_index_271_90(angle, intensity255, region_name_a, region_name_b):
    region_a = COLOR_REGIONS[_normalize_name(region_name_a)]
    touch_a = _get_touch_channel_index(intensity255, region_name_a)
    touch_b = _get_touch_channel_index(intensity255, region_name_b)
    a = float(int(angle) % 360)
    d3 = a + 360.0 if a < 15.0 else a
    span = abs(float(region_a["high_angle"]) - float(region_a["low_angle"]))
    ratio = abs(float(region_a["high_angle"]) - d3) / span if span > 0.0 else 0.0
    if ratio < 0.0:
        ratio = 0.0
    if ratio > 1.0:
        ratio = 1.0
    out = []
    for i in range(12):
        va = int((1.0 - ratio) * touch_a[i])
        vb = int(ratio * touch_b[i])
        vv = va + vb
        out.append(vv if vv >= 1 else 1)
    return out

def _angle_to_color_pair(angle):
    a = int(angle) % 360
    if 270 <= a < 285:
        return ("Indigo", "Purple")
    if 285 <= a < 325:
        return ("Purple", "Red")
    if 325 <= a < 340:
        return ("Red", "Orange")
    if 340 <= a < 360 or 0 <= a < 15:
        return ("Orange", "Yellow")
    if 15 <= a < 65:
        return ("Yellow", "Green")
    if 65 <= a < 90:
        return ("Green", "Blue")
    if a == 90:
        return ("Blue", "Sky Blue")
    if 90 < a < 110:
        return ("Sky Blue", "Ocean Blue 7")
    if 110 <= a < 130:
        return ("Ocean Blue 7", "Ocean Blue 6")
    if 130 <= a < 150:
        return ("Ocean Blue 6", "Ocean Blue 5")
    if 150 <= a < 170:
        return ("Ocean Blue 5", "Ocean Blue 4")
    if 170 <= a < 190:
        return ("Ocean Blue 4", "Ocean Blue 3")
    if 190 <= a < 210:
        return ("Ocean Blue 3", "Ocean Blue 2")
    if 210 <= a < 230:
        return ("Ocean Blue 2", "Ocean Blue 1")
    if 230 <= a < 250:
        return ("Ocean Blue 1", "Deep Ocean Blue")
    if 250 <= a < 270:
        return ("Deep Ocean Blue", "Indigo")
    raise ValueError("unsupported angle %s" % angle)

def _channels_for_angle(angle, intensity=100):
    angle = int(angle) % 360
    intensity255 = _pct_to_device_byte(intensity)
    color_a, color_b = _angle_to_color_pair(angle)
    if intensity255 <= 0:
        return [1] * 12
    if angle <= 90 or angle > 269:
        channels = _get_touch_channel_index_271_90(angle, intensity255, color_a, color_b)
    else:
        channels = _get_touch_channel_index(intensity255, color_a)
    return _check_calibration_data650(channels)

def _row_scaled(row_index, intensity=100):
    src = CALIBRATION[int(row_index)]
    intensity255 = _pct_to_device_byte(intensity)
    if intensity255 <= 0:
        return [1] * 12
    factor = intensity255 / 255.0
    out = []
    for v in src:
        vv = int(round(v * factor))
        out.append(_clamp_u16(vv if vv > 0 else 1))
    return out

@service
def ap700_on(host=None, **kwargs):
    return _send_all(_cmd_power(1), host=host)

@service
def ap700_off(host=None, **kwargs):
    return _send_all(_cmd_power(0), host=host)

@service
def ap700_power(state=1, host=None, **kwargs):
    return _send_all(_cmd_power(state), host=host)

@service
def ap700_find_me(host=None, **kwargs):
    return _send_all(_cmd_find_me(), host=host)

@service
def ap700_send_hex(hex_payload="", host=None, **kwargs):
    s = str(hex_payload).strip().replace(" ", "").replace("0x", "")
    if not s:
        raise ValueError("hex_payload is required")
    if len(s) % 2 != 0:
        raise ValueError("hex_payload must contain an even number of hex digits")
    return _send_all(bytes.fromhex(s), host=host)

@service
def ap700_set_channels12(channels=None, apply_flag=1, host=None, **kwargs):
    vals = list(channels) if not isinstance(channels, str) else [int(x.strip(), 0) for x in channels.split(",") if x.strip()]
    if len(vals) != 12:
        raise ValueError("channels must contain exactly 12 values")
    device_byte = _pct_to_device_byte(apply_flag)
    return _send_all(_cmd_set_all_channel_index(vals, apply_flag=device_byte), host=host)

@service
def ap700_set_calibration_row(row=0, intensity=100, host=None, **kwargs):
    row = int(row)
    if row < 0 or row >= len(CALIBRATION):
        raise ValueError("row must be between 0 and %s" % (len(CALIBRATION) - 1))
    device_byte = _pct_to_device_byte(intensity)
    return _send_all(_cmd_set_all_channel_index(_row_scaled(row, intensity=intensity), apply_flag=device_byte), host=host)

@service
def ap700_set_angle(angle=200, intensity=100, host=None, **kwargs):
    device_byte = _pct_to_device_byte(intensity)
    return _send_all(_cmd_set_all_channel_index(_channels_for_angle(angle, intensity=intensity), apply_flag=device_byte), host=host)

@service
def ap700_set_color_name(color_name="Ocean Blue 3", intensity=100, host=None, **kwargs):
    key = _normalize_name(color_name)
    if key not in COLOR_REGIONS:
        raise ValueError("Unknown color_name: %s" % color_name)
    region = COLOR_REGIONS[key]
    angle = int(round((region["high_angle"] + region["low_angle"]) / 2.0)) % 360
    if key == "blue":
        angle = 90
    if key == "indigo":
        angle = 270
    if key == "moon red":
        return _send_all(_cmd_set_all_channel_index(_row_scaled(28, intensity=intensity), apply_flag=1), host=host)
    if key in ("mood blue", "moon blue"):
        return _send_all(_cmd_set_all_channel_index(_row_scaled(31, intensity=intensity), apply_flag=1), host=host)
    return ap700_set_angle(angle=angle, intensity=intensity, host=host)

@service
def ap700_color_names(**kwargs):
    names = []
    seen = set()
    for key in sorted(COLOR_REGIONS.keys()):
        display = COLOR_REGIONS[key]["name"]
        if display not in seen:
            names.append(display)
            seen.add(display)
    return names
