import machine
import onewire
import ds18x20
import network
import socket
import time
import ntptime
import json
import os
import gc

# ── Configuration ─────────────────────────────────────────────────────────────

def load_config():
    with open('config.json', 'r') as f:
        return json.load(f)

CONFIG = load_config()

SSID           = CONFIG['wifi_ssid']
PASSWORD       = CONFIG['wifi_password']
LOG_INTERVAL   = CONFIG['log_interval_seconds']
MAX_DAYS       = CONFIG['max_days']
MAX_READINGS   = (MAX_DAYS * 24 * 60 * 60) // 300   # based on 5-min production interval
DATA_FILE      = 'data.csv'

# ── Hardware ──────────────────────────────────────────────────────────────────

# XIAO ESP32-C3: D0 = GPIO2, D1 = GPIO3
ds_pin    = machine.Pin(2)
ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))
led       = machine.Pin(3, machine.Pin.OUT)

def read_temperature():
    roms = ds_sensor.scan()
    if not roms:
        return None
    ds_sensor.convert_temp()
    time.sleep_ms(750)
    temp = ds_sensor.read_temp(roms[0])
    return round(temp, 2)

# ── WiFi ──────────────────────────────────────────────────────────────────────

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(False)
    time.sleep(1)
    wlan.active(True)
    time.sleep(1)
    print('Connecting to WiFi...')
    wlan.connect(SSID, PASSWORD)
    timeout = 20
    while not wlan.isconnected() and timeout > 0:
        time.sleep(1)
        timeout -= 1
        print('.', end='')
    if wlan.isconnected():
        print('\nConnected:', wlan.ifconfig()[0])
    else:
        print('\nWiFi connection failed — running without network')
    return wlan

# ── NTP / Time ────────────────────────────────────────────────────────────────

def sync_time():
    try:
        ntptime.settime()
        print('NTP synced')
    except Exception as e:
        print('NTP sync failed:', e)

def timestamp():
    """Return current time as ISO-ish string: YYYY-MM-DD HH:MM:SS (local time)"""
    offset_secs = CONFIG.get('utc_offset_hours', 0) * 3600
    t = time.localtime(time.time() + offset_secs)
    return '{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(
        t[0], t[1], t[2], t[3], t[4], t[5])

def today_str():
    offset_secs = CONFIG.get('utc_offset_hours', 0) * 3600
    t = time.localtime(time.time() + offset_secs)
    return '{:04d}-{:02d}-{:02d}'.format(t[0], t[1], t[2])

# ── Data Logging ──────────────────────────────────────────────────────────────

def init_data_file():
    try:
        os.stat(DATA_FILE)
    except OSError:
        with open(DATA_FILE, 'w') as f:
            f.write('timestamp,temp_c,temp_f\n')

def count_lines():
    count = 0
    try:
        with open(DATA_FILE, 'r') as f:
            for _ in f:
                count += 1
    except OSError:
        pass
    return count

def trim_oldest(keep_lines):
    """Drop oldest readings, keeping header + keep_lines data rows."""
    lines = []
    with open(DATA_FILE, 'r') as f:
        lines = f.readlines()
    if len(lines) <= keep_lines + 1:  # +1 for header
        return
    lines = [lines[0]] + lines[-(keep_lines):]  # header + newest N lines
    with open(DATA_FILE, 'w') as f:
        f.writelines(lines)

def log_reading(temp_c):
    ts = timestamp()
    temp_f = round(temp_c * 9 / 5 + 32, 2)
    with open(DATA_FILE, 'a') as f:
        f.write('{},{},{}\n'.format(ts, temp_c, temp_f))
    total = count_lines() - 1  # subtract header
    if total > MAX_READINGS:
        trim_oldest(MAX_READINGS)
    return ts, temp_c, temp_f

# ── Data Helpers ──────────────────────────────────────────────────────────────

def load_chart_data(max_points=288):
    """Load up to max_points readings from CSV. Returns (labels, temps_c)."""
    labels = []
    temps  = []
    try:
        with open(DATA_FILE, 'r') as f:
            next(f)  # skip header
            for line in f:
                line = line.strip()
                if not line:
                    continue
                parts = line.split(',')
                if len(parts) >= 2:
                    labels.append(parts[0])
                    try:
                        temps.append(float(parts[1]))
                    except ValueError:
                        pass
    except OSError:
        pass
    return labels[-max_points:], temps[-max_points:]

def get_today_minmax():
    today = today_str()
    readings = []
    try:
        with open(DATA_FILE, 'r') as f:
            next(f)
            for line in f:
                line = line.strip()
                if line.startswith(today):
                    parts = line.split(',')
                    if len(parts) >= 2:
                        try:
                            readings.append(float(parts[1]))
                        except ValueError:
                            pass
    except OSError:
        pass
    if readings:
        return min(readings), max(readings)
    return None, None

# ── JSON array helpers (no full-list allocation) ──────────────────────────────

def send_json_strings(conn, items):
    """Send a JSON string array element by element — never allocates the full string."""
    conn.send(b'[')
    for i, item in enumerate(items):
        if i > 0:
            conn.send(b',')
        conn.send(b'"')
        conn.send(item.encode('utf-8'))
        conn.send(b'"')
    conn.send(b']')

def send_json_numbers(conn, items):
    """Send a JSON number array element by element — never allocates the full string."""
    conn.send(b'[')
    for i, item in enumerate(items):
        if i > 0:
            conn.send(b',')
        conn.send(str(item).encode('utf-8'))
    conn.send(b']')

# ── Dashboard (streaming) ─────────────────────────────────────────────────────

def stream_dashboard(conn, current_temp, current_ts, min_today, max_today,
                     chart_labels, chart_data, unit='F'):
    """Stream dashboard HTML to conn in small chunks.
    HTTP header must be sent before calling this function."""

    if unit == 'F':
        display_temp = str(round(current_temp * 9 / 5 + 32, 1)) if current_temp else '--'
        display_min  = str(round(min_today * 9 / 5 + 32, 1)) if min_today is not None else '--'
        display_max  = str(round(max_today * 9 / 5 + 32, 1)) if max_today is not None else '--'
        unit_label   = 'F'
        chart_values = [round(v * 9 / 5 + 32, 1) for v in chart_data]
    else:
        display_temp = str(current_temp) if current_temp else '--'
        display_min  = str(min_today) if min_today is not None else '--'
        display_max  = str(max_today) if max_today is not None else '--'
        unit_label   = 'C'
        chart_values = chart_data

    unit_f_active = 'active' if unit == 'F' else ''
    unit_c_active = 'active' if unit == 'C' else ''
    ts_str        = current_ts or '--'

    def send(s):
        conn.send(s.encode('utf-8') if isinstance(s, str) else s)

    gc.collect()

    # ── <head> & styles ───────────────────────────────────────────────────
    send('<!DOCTYPE html><html lang="en"><head>'
         '<meta charset="UTF-8">'
         '<meta name="viewport" content="width=device-width,initial-scale=1.0">'
         '<title>Freezer Monitor</title>'
         '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>'
         '<style>'
         '*{box-sizing:border-box;margin:0;padding:0;}'
         'body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;padding:24px 16px;}'
         'h1{font-size:1.2rem;color:#94a3b8;text-align:center;margin-bottom:24px;letter-spacing:.1em;text-transform:uppercase;}'
         '.temp-big{text-align:center;font-size:6rem;font-weight:700;color:#38bdf8;line-height:1;margin-bottom:8px;}'
         '.temp-ts{text-align:center;color:#475569;font-size:.85rem;margin-bottom:24px;}'
         '.stats{display:flex;justify-content:center;gap:32px;margin-bottom:32px;}'
         '.stat{text-align:center;}'
         '.stat-label{font-size:.75rem;color:#64748b;text-transform:uppercase;letter-spacing:.05em;}'
         '.stat-value{font-size:1.5rem;font-weight:600;color:#cbd5e1;}'
         '.stat-value.cold{color:#38bdf8;}.stat-value.warm{color:#fb923c;}'
         '.chart-card{background:#1e293b;border-radius:12px;padding:20px;max-width:700px;margin:0 auto;}'
         '.chart-controls{display:flex;gap:8px;margin-bottom:16px;justify-content:flex-end;}'
         '.btn{padding:4px 12px;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#94a3b8;font-size:.8rem;cursor:pointer;}'
         '.btn.active{background:#0ea5e9;color:#fff;border-color:#0ea5e9;}'
         '.unit-toggle{display:flex;justify-content:center;gap:8px;margin-bottom:24px;}'
         'canvas{max-height:260px;}'
         '.footer{text-align:center;color:#334155;font-size:.75rem;margin-top:24px;}'
         '</style></head><body>')
    gc.collect()

    # ── Body ──────────────────────────────────────────────────────────────
    send('<h1>Freezer Monitor</h1>'
         '<div class="temp-big">' + display_temp + '&deg;' + unit_label + '</div>'
         '<div class="temp-ts">Last reading: ' + ts_str + '</div>'
         '<div class="unit-toggle">'
         '<button class="btn ' + unit_f_active + '" onclick="switchUnit(\'F\')">F</button>'
         '<button class="btn ' + unit_c_active + '" onclick="switchUnit(\'C\')">C</button>'
         '</div>'
         '<div class="stats">'
         '<div class="stat"><div class="stat-label">Today Low</div>'
         '<div class="stat-value cold">' + display_min + '&deg;' + unit_label + '</div></div>'
         '<div class="stat"><div class="stat-label">Today High</div>'
         '<div class="stat-value warm">' + display_max + '&deg;' + unit_label + '</div></div>'
         '</div>'
         '<div class="chart-card">'
         '<div class="chart-controls">'
         '<button class="btn active" onclick="setRange(24)" id="btn24">24h</button>'
         '<button class="btn" onclick="setRange(168)" id="btn7d">7d</button>'
         '<button class="btn" onclick="setRange(720)" id="btn30d">30d</button>'
         '</div>'
         '<canvas id="tempChart"></canvas></div>'
         '<div class="footer">Auto-refreshes every 60s &nbsp;&middot;&nbsp;'
         '<a href="/data.csv" style="color:#334155">Download CSV</a></div>')
    gc.collect()

    # ── Script: chart data sent element by element ────────────────────────
    send('<script>')
    send('const ALL_LABELS=')
    send_json_strings(conn, chart_labels)
    send(';const ALL_VALUES=')
    send_json_numbers(conn, chart_values)
    send(';const ALL_VALUES_C=')
    send_json_numbers(conn, chart_data)
    gc.collect()
    send(';let currentUnit="' + unit + '";let chart;'
         'function initChart(labels,values){'
         'const ctx=document.getElementById("tempChart").getContext("2d");'
         'chart=new Chart(ctx,{type:"line",data:{labels:labels,datasets:[{'
         'label:"Temperature",data:values,borderColor:"#38bdf8",'
         'backgroundColor:"rgba(56,189,248,0.08)",borderWidth:2,'
         'pointRadius:values.length>100?0:3,pointHoverRadius:5,fill:true,tension:0.3}]},'
         'options:{responsive:true,plugins:{legend:{display:false},'
         'tooltip:{callbacks:{label:ctx=>ctx.parsed.y.toFixed(1)+"°"+currentUnit}}},'
         'scales:{x:{ticks:{color:"#475569",maxTicksLimit:8,maxRotation:0},'
         'grid:{color:"#1e293b"}},y:{ticks:{color:"#475569"},grid:{color:"#334155"}}}}});}'
         'function setRange(hours){'
         'const labels=ALL_LABELS.slice(-Math.min(hours*12,ALL_LABELS.length));'
         'const rawC=ALL_VALUES_C.slice(-Math.min(hours*12,ALL_VALUES_C.length));'
         'const values=currentUnit==="F"?rawC.map(v=>Math.round(v*9/5*10+320)/10):rawC;'
         'chart.data.labels=labels;chart.data.datasets[0].data=values;'
         'chart.data.datasets[0].pointRadius=labels.length>100?0:3;chart.update();'
         'document.querySelectorAll(".chart-controls .btn").forEach(b=>b.classList.remove("active"));'
         'const map={24:"btn24",168:"btn7d",720:"btn30d"};'
         'document.getElementById(map[hours]).classList.add("active");}'
         'function switchUnit(u){currentUnit=u;window.location.href="/?unit="+u;}'
         'initChart(ALL_LABELS.slice(-288),ALL_VALUES.slice(-288));'
         'setTimeout(()=>location.reload(),60000);'
         '</script><script src="/js/easter-egg-injector.js" defer></script></body></html>')

# ── Web Server ────────────────────────────────────────────────────────────────

def serve():
    addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(1)
    s.settimeout(0)  # non-blocking
    return s

def handle_request(conn, current_temp, current_ts):
    try:
        request = conn.recv(1024).decode('utf-8')

        # Parse unit preference
        unit = 'F'
        if '?unit=C' in request or 'unit=C' in request:
            unit = 'C'

        if 'GET /data.csv' in request:
            conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/csv\r\nContent-Disposition: attachment; filename="data.csv"\r\n\r\n')
            with open(DATA_FILE, 'r') as f:
                while True:
                    chunk = f.read(512)
                    if not chunk:
                        break
                    conn.send(chunk)

        elif 'GET /' in request:
            # Send HTTP header immediately so client doesn't time out while
            # we load data and build the page
            conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n')
            gc.collect()
            labels, temps_c = load_chart_data(max_points=288)
            min_t, max_t = get_today_minmax()
            gc.collect()
            stream_dashboard(conn, current_temp, current_ts, min_t, max_t,
                             labels, temps_c, unit)

        else:
            conn.send(b'HTTP/1.1 404 Not Found\r\n\r\nNot found')

    except Exception as e:
        print('Request error:', e)
    finally:
        conn.close()

# ── Main Loop ─────────────────────────────────────────────────────────────────

def main():
    led.value(1)
    init_data_file()

    wlan = connect_wifi()
    if wlan.isconnected():
        sync_time()
        print('Dashboard at http://{}/'.format(wlan.ifconfig()[0]))

    server_sock = serve()

    current_temp = None
    current_ts   = None
    last_log     = time.time() - LOG_INTERVAL  # log immediately on first pass

    print('Running. Log interval: {}s'.format(LOG_INTERVAL))

    while True:
        now = time.time()

        # ── Sensor read & log ──────────────────────────────────────────────
        if now - last_log >= LOG_INTERVAL:
            temp = read_temperature()
            if temp is not None:
                current_ts, current_temp, _ = log_reading(temp)
                print('{} — {:.2f}°C / {:.1f}°F'.format(
                    current_ts, temp, temp * 9/5 + 32))
                #led.value(1)
                #time.sleep_ms(80)
                #led.value(0)
            else:
                print('Sensor not found')
            last_log = now

        # ── Web server — non-blocking accept ──────────────────────────────
        try:
            conn, addr = server_sock.accept()
            conn.settimeout(10)
            handle_request(conn, current_temp, current_ts)
        except OSError:
            pass  # no incoming connection, that's fine

        time.sleep_ms(100)

main()
