#!/usr/bin/env python3
"""
RobinsFire Hub Server
- Serves static HTML files
- Proxies FastField API calls (CORS workaround)
- Runs FRA Processor (local only - no-op on Railway)
- Polls Gmail inbox via IMAP and parses FastField stage emails into job cards
"""
import http.server
import urllib.request
import urllib.error
import ssl
import json
import os
import re
import imaplib
import email as email_lib
from email.header import decode_header
from datetime import datetime, timezone
import threading
import time

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
PORT = int(os.environ.get('PORT', 8080))
FF_API = 'https://api.fastfieldforms.com/services/v3'

GMAIL_USER = os.environ.get('GMAIL_USER', '')          # e.g. marco.fiore@cambsfiresafe.com
GMAIL_APP_PASSWORD = os.environ.get('GMAIL_APP_PASSWORD', '')
FASTFIELD_SENDER = 'robinsfield@robinsfiresolutions.com'

POLL_INTERVAL = int(os.environ.get('GMAIL_POLL_INTERVAL', 120))  # seconds between polls

# ---------------------------------------------------------------------------
# SSL context (permissive - avoids cert issues on Mac and Railway)
# ---------------------------------------------------------------------------
ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE

# ---------------------------------------------------------------------------
# Jobs cache - rebuilt on each poll
# ---------------------------------------------------------------------------
_jobs_lock = threading.Lock()
_jobs_cache = []
_last_polled = None
_poll_error = None

# ---------------------------------------------------------------------------
# Email stage patterns
# Each tuple: (compiled_regex, stage_key, colour, label)
# Address is always capture group 1
# ---------------------------------------------------------------------------
STAGE_PATTERNS = [
    (
        re.compile(r'^Inspection Notes Form\s*[—\-–]+\s*(.+)$', re.IGNORECASE),
        'notes_dispatched', 'blue', 'Site notes dispatched'
    ),
    (
        re.compile(r'^FRA Report Ready for Review\s*-{1,2}\s*(.+)$', re.IGNORECASE),
        'report_ready', 'orange', 'Report ready for review'
    ),
    (
        re.compile(r'^Report for (.+?) Returned for Review or Changes by .+$', re.IGNORECASE),
        'returned', 'red', 'Returned for changes'
    ),
    (
        re.compile(r'^Approval Stage for\s*:\s*(.+)$', re.IGNORECASE),
        'with_reviewer', 'purple', 'With reviewer'
    ),
    (
        re.compile(r'^The Report for (.+?) is ready for Admin Review$', re.IGNORECASE),
        'with_admin', 'green', 'With admin'
    ),
]

# Ordered list of stages for "most advanced" logic
STAGE_ORDER = ['notes_dispatched', 'report_ready', 'returned', 'with_reviewer', 'with_admin']

COLOUR_HEX = {
    'blue':   '#3b82f6',
    'orange': '#f5a020',
    'red':    '#ef4444',
    'purple': '#a855f7',
    'green':  '#22c55e',
}


# Map original recipient email → assessor display name
ASSESSOR_EMAILS = {
    'marco.fiore@cambsfiresafe.com': 'Marco',
    'neil.green@cambsfiresafe.com': 'Neil',
}

def extract_original_to(msg):
    """
    Forwarded emails carry the original recipient in one of several headers.
    Try them in order of reliability.
    Returns a lowercase email string or '' if not found.
    """
    for header in ['X-Forwarded-To', 'X-Original-To', 'Delivered-To', 'To']:
        val = msg.get(header, '')
        if val:
            m = re.search(r'[\w.+-]+@[\w.-]+', val)
            if m:
                addr = m.group(0).lower()
                if addr in ASSESSOR_EMAILS:
                    return addr
    return ''


def decode_subject(raw_subject):
    """Decode an email Subject header to a plain string."""
    parts = decode_header(raw_subject or '')
    decoded = []
    for part, charset in parts:
        if isinstance(part, bytes):
            decoded.append(part.decode(charset or 'utf-8', errors='replace'))
        else:
            decoded.append(part)
    return ''.join(decoded).strip()


def parse_date(date_str):
    """Parse an email Date header to a UTC ISO string. Returns '' on failure."""
    try:
        from email.utils import parsedate_to_datetime
        dt = parsedate_to_datetime(date_str)
        return dt.astimezone(timezone.utc).isoformat()
    except Exception:
        return ''


def match_subject(subject):
    """
    Try all stage patterns against a subject line.
    Strips Fwd:/Re: prefixes first.
    Returns (stage_key, colour, label, premises) or None.
    """
    # Strip any number of Fwd: / Re: prefixes
    clean = re.sub(r'^(Fwd?:|Re:)\s*', '', subject, flags=re.IGNORECASE).strip()
    for pattern, stage_key, colour, label in STAGE_PATTERNS:
        m = pattern.match(clean)
        if m:
            premises = m.group(1).strip().rstrip('.')
            return stage_key, colour, label, premises
    return None


def poll_gmail():
    """
    Connect to Gmail via IMAP SSL, fetch all emails from the FastField sender,
    parse stage patterns, and return (jobs_list, error_or_None).
    """
    if not GMAIL_USER or not GMAIL_APP_PASSWORD:
        return [], 'Gmail credentials not configured (set GMAIL_USER and GMAIL_APP_PASSWORD env vars)'

    try:
        mail = imaplib.IMAP4_SSL('imap.gmail.com')
        mail.login(GMAIL_USER, GMAIL_APP_PASSWORD)
        mail.select('inbox')

        status, data = mail.search(None, 'ALL')
        if status != 'OK':
            mail.logout()
            return [], f'IMAP search failed: {status}'

        msg_ids = data[0].split()
        print(f'[Gmail] Found {len(msg_ids)} emails total, checking subjects...')

        events = []
        for msg_id in msg_ids:
            status, msg_data = mail.fetch(msg_id, '(RFC822)')
            if status != 'OK':
                continue
            raw = msg_data[0][1]
            msg = email_lib.message_from_bytes(raw)
            subject = decode_subject(msg.get('Subject', ''))
            date_iso = parse_date(msg.get('Date', ''))

            result = match_subject(subject)
            print(f'[Gmail] Subject: "{subject}" -> {"MATCH: "+result[0] if result else "no match"}')
            if result:
                stage_key, colour, label, premises = result
                original_to = extract_original_to(msg)
                assessor_email = original_to
                assessor_name = ASSESSOR_EMAILS.get(original_to, '')
                events.append({
                    'premises': premises,
                    'stage_key': stage_key,
                    'colour': colour,
                    'label': label,
                    'date_iso': date_iso,
                    'subject': subject,
                    'assessor_email': assessor_email,
                    'assessor_name': assessor_name,
                })

        mail.logout()

        # Group by premises (case-insensitive key), build job cards
        jobs_by_key = {}
        for ev in sorted(events, key=lambda x: x['date_iso']):
            key = ev['premises'].lower()
            if key not in jobs_by_key:
                jobs_by_key[key] = {
                    'premises': ev['premises'],
                    'current_stage': None,
                    'current_colour': None,
                    'current_label': None,
                    'assessor_email': ev['assessor_email'],
                    'assessor_name': ev['assessor_name'],
                    'history': [],
                }
            job = jobs_by_key[key]

            # Advance current stage if this event is further along the workflow
            new_order = STAGE_ORDER.index(ev['stage_key']) if ev['stage_key'] in STAGE_ORDER else -1
            cur_order = STAGE_ORDER.index(job['current_stage']) if job['current_stage'] in STAGE_ORDER else -1
            if new_order >= cur_order:
                job['current_stage'] = ev['stage_key']
                job['current_colour'] = ev['colour']
                job['current_label'] = ev['label']
                # Update assessor from most recent event if we have one
                if ev['assessor_email']:
                    job['assessor_email'] = ev['assessor_email']
                    job['assessor_name'] = ev['assessor_name']

            job['history'].append({
                'stage_key': ev['stage_key'],
                'label': ev['label'],
                'colour': ev['colour'],
                'date_iso': ev['date_iso'],
            })

        jobs = list(jobs_by_key.values())

        # Sort: non-admin jobs first, then by stage depth desc, then by recency
        def sort_key(j):
            is_done = 1 if j['current_stage'] == 'with_admin' else 0
            stage_depth = STAGE_ORDER.index(j['current_stage']) if j['current_stage'] in STAGE_ORDER else 0
            latest = j['history'][-1]['date_iso'] if j['history'] else ''
            return (is_done, -stage_depth, latest)

        jobs.sort(key=sort_key)
        return jobs, None

    except imaplib.IMAP4.error as e:
        return [], f'IMAP authentication error: {e}'
    except Exception as e:
        return [], f'Gmail poll error: {e}'


def background_poller():
    """Daemon thread: polls Gmail every POLL_INTERVAL seconds."""
    global _jobs_cache, _last_polled, _poll_error
    while True:
        print(f'[Gmail] Polling inbox...')
        jobs, err = poll_gmail()
        with _jobs_lock:
            if err:
                _poll_error = err
                print(f'[Gmail] Error: {err}')
            else:
                _jobs_cache = jobs
                _poll_error = None
                print(f'[Gmail] OK — {len(jobs)} jobs loaded')
            _last_polled = datetime.now(timezone.utc).isoformat()
        time.sleep(POLL_INTERVAL)


# ---------------------------------------------------------------------------
# HTTP request handler
# ---------------------------------------------------------------------------
class FRAHandler(http.server.SimpleHTTPRequestHandler):

    def log_message(self, format, *args):
        super().log_message(format, *args)

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

    def do_GET(self):
        if self.path == '/api/jobs':
            self._serve_jobs()
        elif self.path == '/api/jobs/refresh':
            self._refresh_jobs()
        elif self.path == '/health':
            self._json_response(200, {'ok': True, 'service': 'robinsfire-hub'})
        else:
            super().do_GET()

    def do_POST(self):
        if self.path.startswith('/ff-proxy/'):
            self._ff_proxy()
        elif self.path == '/run-fra-processor':
            self._run_fra_processor()
        else:
            self.send_response(404)
            self._cors()
            self.end_headers()

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------
    def _cors(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS')
        self.send_header('Access-Control-Allow-Headers',
                         'Content-Type, FastField-API-Key, X-Gatekeeper-SessionToken, Authorization, X-Method')

    def _json_response(self, status, data):
        body = json.dumps(data).encode()
        self.send_response(status)
        self._cors()
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    # ------------------------------------------------------------------
    # Jobs endpoints
    # ------------------------------------------------------------------
    def _serve_jobs(self):
        with _jobs_lock:
            payload = {
                'jobs': list(_jobs_cache),
                'last_polled': _last_polled,
                'error': _poll_error,
                'colour_hex': COLOUR_HEX,
            }
        self._json_response(200, payload)

    def _refresh_jobs(self):
        """Force an immediate re-poll (UI refresh button)."""
        global _jobs_cache, _last_polled, _poll_error
        jobs, err = poll_gmail()
        with _jobs_lock:
            if not err:
                _jobs_cache = jobs
                _poll_error = None
            else:
                _poll_error = err
            _last_polled = datetime.now(timezone.utc).isoformat()
        self._serve_jobs()

    # ------------------------------------------------------------------
    # FRA Processor
    # ------------------------------------------------------------------
    def _run_fra_processor(self):
        is_railway = bool(
            os.environ.get('RAILWAY_ENVIRONMENT') or
            os.environ.get('RAILWAY_PROJECT_ID') or
            os.environ.get('RAILWAY_SERVICE_NAME')
        )
        if is_railway:
            self._json_response(200, {
                'ok': False,
                'railway': True,
                'error': 'FRA Processor requires the local server. Run fra_server.py on your Mac to use this feature.'
            })
            return

        # Local Mac path
        try:
            import subprocess
            script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fra_processor.sh')
            if os.path.exists(script):
                subprocess.Popen(['open', '-a', 'Terminal', script])
                self._json_response(200, {'ok': True})
            else:
                self._json_response(200, {'ok': False, 'error': 'fra_processor.sh not found.'})
        except Exception as e:
            self._json_response(500, {'ok': False, 'error': str(e)})

    # ------------------------------------------------------------------
    # FastField proxy
    # ------------------------------------------------------------------
    def _ff_proxy(self):
        ff_path = self.path[len('/ff-proxy'):]
        ff_url = FF_API + ff_path

        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length) if length else b'{}'

        fwd_headers = {'Content-Type': 'application/json'}
        for h in ['FastField-API-Key', 'X-Gatekeeper-SessionToken', 'Authorization']:
            val = self.headers.get(h)
            if val:
                fwd_headers[h] = val

        method = self.headers.get('X-Method', 'POST')
        print(f'  Proxy: {method} {ff_url}')

        try:
            req = urllib.request.Request(ff_url, data=body, headers=fwd_headers, method=method)
            with urllib.request.urlopen(req, context=ssl_ctx) as resp:
                status = resp.status
                data = resp.read()
                print(f'  Response: {status} -- {data[:100]}')
        except urllib.error.HTTPError as e:
            status = e.code
            data = e.read()
            print(f'  HTTP Error: {status} -- {data[:100]}')
        except Exception as e:
            print(f'  Exception: {e}')
            self.send_response(502)
            self._cors()
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'error': str(e)}).encode())
            return

        self.send_response(status)
        self._cors()
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(data)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == '__main__':
    os.chdir(os.path.dirname(os.path.abspath(__file__)))

    if GMAIL_USER and GMAIL_APP_PASSWORD:
        t = threading.Thread(target=background_poller, daemon=True)
        t.start()
        print(f'[Gmail] Background poller started — interval: {POLL_INTERVAL}s, user: {GMAIL_USER}')
    else:
        print('[Gmail] Credentials not set. Add GMAIL_USER and GMAIL_APP_PASSWORD to your Railway env vars.')

    server = http.server.HTTPServer(('', PORT), FRAHandler)
    print(f'RobinsFire Hub running on port {PORT}')
    print(f'Endpoints: /ff-proxy/  /api/jobs  /api/jobs/refresh  /run-fra-processor  /health')
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
