#!/usr/bin/env python3
"""
Curzon Trucking Calendar — Local Server
Serves calendar-local.html, proxies AI requests, and generates driver ICS feeds.
"""
import http.server, json, urllib.request, urllib.error, os, re, threading, time, base64, secrets
from datetime import datetime, timezone

PORT       = int(os.environ.get('PORT', 8787))
DIR        = os.path.dirname(os.path.abspath(__file__))
SUPA_URL   = 'https://vgglyebsbbgooqmguzmi.supabase.co'
SUPA_KEY   = 'sb_publishable_M0RNyUH42gtaJXNG_jFNqg_VD4cc6mQ'
MOTIVE_KEY = '023c1b4f-011f-4096-bfce-ee1771c46661'
SUPA_HEADERS = {
    'apikey':        SUPA_KEY,
    'Authorization': f'Bearer {SUPA_KEY}',
}

# ── Auth + API key from environment ───────────────────────────────────────────
AUTH_USER    = os.environ.get('AUTH_USER', 'curzon')
AUTH_PASS    = os.environ.get('AUTH_PASS', 'dispatch')
ANTHROPIC_KEY = os.environ.get('ANTHROPIC_API_KEY', '')

# In-memory session tokens — cleared on server restart, 30-day cookie on client
_sessions = set()

STATUS_LABELS = {
    'enroute':   'En Route',
    'pickedup':  'Picked Up',
    'delivered': 'Delivered',
    'covered':   'Covered',
}

# Motive vehicle_id → calendar resource id
VEHICLE_TO_RID = {
    2161550: 6,   # Julio     (412863)
    2161561: 3,   # Fernando  (149062)
    2161567: 2,   # Eiber     (2024)
    2177626: 5,   # Pablo     (2023)
    2180995: 11,  # Rodrigo   (264495)
    2184525: 4,   # Eduardo   (2022)
    2185065: 9,   # Miguel    (2025)
    2189723: 12,  # Lennin    (431985)
    2197614: 8,   # Alonzo    (2021)
    2199017: 10,  # Albino    (214733)
    2199020: 1,   # Blade     (412803)
    # Kevin (422465) and Edwin (01) have no Motive ELD
}

_locations  = {}   # str(rid) → {location, located_at}
_loc_lock   = threading.Lock()

def _fetch_locations():
    try:
        req = urllib.request.Request(
            'https://api.keeptruckin.com/v1/vehicle_locations?per_page=50',
            headers={'X-Api-Key': MOTIVE_KEY, 'Accept': 'application/json'}
        )
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read())
        new_locs = {}
        for v in data.get('vehicles', []):
            veh = v.get('vehicle', {})
            rid = VEHICLE_TO_RID.get(veh.get('id'))
            if not rid:
                continue
            loc = veh.get('current_location', {})
            if loc.get('description'):
                new_locs[str(rid)] = {
                    'location':   loc['description'],
                    'located_at': loc.get('located_at', ''),
                    'lat':        loc.get('lat'),
                    'lon':        loc.get('lon'),
                }
        with _loc_lock:
            _locations.clear()
            _locations.update(new_locs)
        print(f'  Locations updated: {len(new_locs)} trucks')
    except Exception as e:
        print(f'  Location fetch error: {e}')

def _location_loop():
    while True:
        _fetch_locations()
        time.sleep(1800)   # refresh every 30 minutes

threading.Thread(target=_location_loop, daemon=True).start()

class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=DIR, **kwargs)

    # ── Session / Auth ────────────────────────────────────────────────────────

    def _authorized(self):
        # Check session cookie
        for part in self.headers.get('Cookie', '').split(';'):
            k, _, v = part.strip().partition('=')
            if k.strip() == 'curzon_session' and v.strip() in _sessions:
                return True
        # Fall back to Basic Auth (local dev)
        auth = self.headers.get('Authorization', '')
        if not auth.startswith('Basic '):
            return False
        try:
            decoded = base64.b64decode(auth[6:]).decode('utf-8')
            user, _, passwd = decoded.partition(':')
            return user == AUTH_USER and passwd == AUTH_PASS
        except Exception:
            return False

    def _handle_login(self):
        try:
            length = int(self.headers.get('Content-Length', 0))
            body   = json.loads(self.rfile.read(length))
            if body.get('user') == AUTH_USER and body.get('pass') == AUTH_PASS:
                token = secrets.token_hex(32)
                _sessions.add(token)
                self.send_response(200)
                self._cors()
                self.send_header('Set-Cookie',
                    f'curzon_session={token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=2592000')
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': True}).encode())
            else:
                self._send_json(401, {'error': 'Invalid username or password'})
        except Exception as e:
            self._send_json(500, {'error': str(e)})

    def _send_json(self, code, obj):
        body = json.dumps(obj).encode()
        self.send_response(code)
        self._cors()
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(body)

    def do_OPTIONS(self):
        self.send_response(200)
        self._cors()
        self.end_headers()

    def do_GET(self):
        # ICS feeds are public so Google/Apple Calendar can subscribe without auth
        m = re.match(r'^/feed/(\d+)\.ics$', self.path)
        if m:
            self._serve_ics(int(m.group(1))); return
        # API endpoints require auth
        if self.path.startswith('/api/'):
            if not self._authorized():
                self._send_json(401, {'error': 'unauthorized'}); return
            if self.path == '/api/locations':
                self._serve_locations()
            elif self.path == '/api/ping':
                self._send_json(200, {'ok': True})
            else:
                self.send_error(404)
            return
        # Static files served publicly — login overlay in the HTML handles auth
        super().do_GET()

    def _serve_locations(self):
        with _loc_lock:
            body = json.dumps(_locations).encode()
        self.send_response(200)
        self._cors()
        self.send_header('Content-Type', 'application/json')
        self.send_header('Cache-Control', 'no-cache')
        self.end_headers()
        self.wfile.write(body)

    def do_POST(self):
        if self.path == '/api/login':
            self._handle_login(); return
        if not self._authorized():
            self._send_json(401, {'error': 'unauthorized'}); return
        if self.path == '/api/ai':
            self._proxy_ai()
        elif self.path == '/api/parse-ratecon':
            self._parse_ratecon()
        else:
            self.send_error(404)

    # ── ICS feed ──────────────────────────────────────────────────────────────

    def _serve_ics(self, rid):
        try:
            driver_name = self._fetch_driver_name(rid)
            events      = self._fetch_events(rid)
            body        = self._build_ics(rid, driver_name, events).encode('utf-8')

            self.send_response(200)
            self.send_header('Content-Type', 'text/calendar; charset=utf-8')
            self.send_header('Content-Disposition',
                             f'inline; filename="curzon-driver-{rid}.ics"')
            self.send_header('Cache-Control', 'no-cache, max-age=0')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        except Exception as e:
            self.send_response(500)
            self.end_headers()
            self.wfile.write(str(e).encode())

    def _fetch_driver_name(self, rid):
        try:
            url = f'{SUPA_URL}/rest/v1/driver_overrides?id=eq.{rid}&select=name'
            req = urllib.request.Request(url, headers=SUPA_HEADERS)
            with urllib.request.urlopen(req) as resp:
                rows = json.loads(resp.read())
                if rows and rows[0].get('name'):
                    return rows[0]['name']
        except Exception:
            pass
        return f'Driver {rid}'

    def _fetch_events(self, rid):
        url = f'{SUPA_URL}/rest/v1/events?rid=eq.{rid}&select=*&order=start'
        req = urllib.request.Request(url, headers=SUPA_HEADERS)
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())

    def _build_ics(self, rid, driver_name, events):
        now = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
        lines = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'PRODID:-//Curzon Trucking//Dispatch//EN',
            f'X-WR-CALNAME:{driver_name} \u2014 Loads',
            'X-WR-TIMEZONE:America/Denver',
            'CALSCALE:GREGORIAN',
            'METHOD:PUBLISH',
            'REFRESH-INTERVAL;VALUE=DURATION:PT30M',
            'X-PUBLISHED-TTL:PT30M',
        ]

        for ev in events:
            uid     = ev.get('id', f'{rid}-{ev.get("start","")}')
            dtstart = self._fmt_dt(ev.get('start', ''))
            dtend   = self._fmt_dt(ev.get('end',   ''))

            summary = ev.get('summary') or 'Load'

            desc_parts = []
            if ev.get('load_num'): desc_parts.append(f'Load #: {ev["load_num"]}')
            if ev.get('ref_nums'): desc_parts.append(f'Ref #: {ev["ref_nums"]}')
            if ev.get('status'):
                desc_parts.append(f'Status: {STATUS_LABELS.get(ev["status"], ev["status"])}')
            if ev.get('notes'):   desc_parts.append(f'Notes: {ev["notes"]}')
            description = '\\n'.join(desc_parts)

            lines += [
                'BEGIN:VEVENT',
                f'UID:{uid}@curzon-trucking',
                f'DTSTAMP:{now}',
                f'DTSTART;TZID=America/Denver:{dtstart}',
                f'DTEND;TZID=America/Denver:{dtend}',
                f'SUMMARY:{self._esc(summary)}',
                f'DESCRIPTION:{self._esc(description)}',
                'END:VEVENT',
            ]

        lines.append('END:VCALENDAR')
        return '\r\n'.join(lines)

    def _fmt_dt(self, s):
        return s[:16].replace('-', '').replace(':', '') + '00'

    def _esc(self, s):
        return s.replace('\\', '\\\\').replace('\n', '\\n') \
                .replace(',', '\\,').replace(';', '\\;')

    # ── Rate con parser ───────────────────────────────────────────────────────

    def _parse_ratecon(self):
        try:
            length  = int(self.headers.get('Content-Length', 0))
            body    = json.loads(self.rfile.read(length))
            pdf_b64 = body.get('data', '')

            prompt = (
                'You are parsing a trucking rate confirmation (rate con) document. '
                'Extract the fields below and return ONLY a valid JSON object — no markdown, no explanation.\n\n'
                '{\n'
                '  "summary":  "BROKER NAME first, then short route — e.g. \'Echo: Salt Lake City to Denver\'. '
                'Always lead with the broker name followed by a colon.",\n'
                '  "loadNum":  "load number assigned by the broker or shipper",\n'
                '  "refNums":  "any extra reference numbers (PO, order, etc.) comma-separated",\n'
                '  "start":    "pickup date/time in YYYY-MM-DDTHH:mm (24-hour, Mountain Time)",\n'
                '  "end":      "delivery date/time in YYYY-MM-DDTHH:mm (24-hour, Mountain Time)",\n'
                '  "notes":    "Format exactly as shown, omitting any section not found, with a blank line between each section:\n'
                'Load #: [number]\n'
                '\n'
                'Ref #s: [reference numbers]\n'
                '\n'
                'Appt times: [appointment times if given]\n'
                '\n'
                'Pickup:\n'
                '[full pickup address]\n'
                '\n'
                'Delivery:\n'
                '[full delivery address]\n'
                '\n'
                'Special instructions: [include any instructions relevant to the driver, then EXCLUDE: '
                'insurance requirements, cargo liability, tracking app requirements (e.g. Macropoint, FourKites), '
                'language requirements, paperwork submission deadlines, late fees or fines, '
                'payment terms, broker/carrier agreement terms, and any other administrative or legal language. '
                'If nothing driver-relevant remains, use an empty string.]"\n'
                '}\n\n'
                'IMPORTANT: Do NOT include any rate, price, pay, or dollar amount information anywhere in your response. '
                'Convert all times to Mountain Time (America/Denver). '
                'If a field is not found use an empty string.'
            )

            payload = {
                'model': 'claude-haiku-4-5-20251001',
                'max_tokens': 1024,
                'messages': [{
                    'role': 'user',
                    'content': [
                        {
                            'type': 'document',
                            'source': {
                                'type': 'base64',
                                'media_type': 'application/pdf',
                                'data': pdf_b64,
                            }
                        },
                        {'type': 'text', 'text': prompt}
                    ]
                }]
            }

            req = urllib.request.Request(
                'https://api.anthropic.com/v1/messages',
                data=json.dumps(payload).encode(),
                headers={
                    'Content-Type':      'application/json',
                    'x-api-key':         ANTHROPIC_KEY,
                    'anthropic-version': '2023-06-01',
                },
                method='POST'
            )
            with urllib.request.urlopen(req) as resp:
                result = json.loads(resp.read())

            text = result['content'][0]['text'].strip()
            if text.startswith('```'):
                text = '\n'.join(
                    line for line in text.splitlines()
                    if not line.startswith('```')
                ).strip()

            fields = json.loads(text)
            self.send_response(200)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps(fields).encode())

        except Exception as e:
            self.send_response(500)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'error': str(e)}).encode())

    # ── AI proxy ──────────────────────────────────────────────────────────────

    def _proxy_ai(self):
        try:
            length = int(self.headers.get('Content-Length', 0))
            body   = self.rfile.read(length)

            req = urllib.request.Request(
                'https://api.anthropic.com/v1/messages',
                data=body,
                headers={
                    'Content-Type':      'application/json',
                    'x-api-key':         ANTHROPIC_KEY,
                    'anthropic-version': '2023-06-01',
                },
                method='POST'
            )
            with urllib.request.urlopen(req) as resp:
                data = resp.read()
            self.send_response(200)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(data)

        except urllib.error.HTTPError as e:
            data = e.read()
            self.send_response(e.code)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(data)

        except Exception as e:
            self.send_response(500)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'error': {'message': str(e)}}).encode())

    def _cors(self):
        self.send_header('Access-Control-Allow-Origin',  '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key')

    def log_message(self, fmt, *args):
        print(f"  {fmt % args}")

if __name__ == '__main__':
    with http.server.HTTPServer(('', PORT), Handler) as httpd:
        print(f"\n  Curzon Calendar -> http://localhost:{PORT}/calendar-local.html")
        print("  Press Ctrl+C to stop.\n")
        httpd.serve_forever()
