from odoo import models, fields, api, _
from odoo.exceptions import UserError
import json
import base64
import os
import re
from datetime import datetime, timedelta
from math import radians, sin, cos, sqrt, atan2
from collections import Counter

VEHICLE_ACTIVITIES = {
    'IN_VEHICLE', 'IN_ROAD_VEHICLE', 'IN_RAIL_VEHICLE',
    'IN_TWO_WHEELER_VEHICLE', 'IN_PASSENGER_VEHICLE',
}
WALKING_ACTIVITIES = {'WALKING', 'ON_FOOT', 'RUNNING'}
CYCLING_ACTIVITIES = {'ON_BICYCLE', 'CYCLING'}

SEMANTIC_TYPE_CATEGORY = {
    'HOME': 'Home',
    'INFERRED_HOME': 'Home',
    'WORK': 'Work',
    'INFERRED_WORK': 'Work',
    'SEARCHED_ADDRESS': 'Searched Address',
    'ALIASED_LOCATION': 'Saved Place',
    'UNKNOWN': 'Unknown',
}


def _haversine_miles(lat1, lon1, lat2, lon2):
    R = 3958.8
    lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
    dlat, dlon = lat2 - lat1, lon2 - lon1
    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    return R * 2 * atan2(sqrt(a), sqrt(1 - a))


def _distance_meters(lat1, lon1, lat2, lon2):
    return _haversine_miles(lat1, lon1, lat2, lon2) * 1609.34


def _activity_type_to_travel_mode(activity_type):
    if not activity_type:
        return None
    t = activity_type.upper()
    if 'UNKNOWN' in t:
        return None
    if t == 'IN_RAIL_VEHICLE':
        return 'transit'
    if t in VEHICLE_ACTIVITIES:
        return 'driving'
    if t in WALKING_ACTIVITIES:
        return 'walking'
    if t in CYCLING_ACTIVITIES:
        return 'cycling'
    return None


def _speed_to_travel_mode(max_speed_mph):
    """Infer travel mode from maximum speed observed."""
    if max_speed_mph > 150:
        return 'flying'
    if max_speed_mph > 15:
        return 'driving'
    if max_speed_mph > 3:
        return 'cycling'
    if max_speed_mph > 0:
        return 'walking'
    return 'unknown'


def _parse_latlng(latlng_str):
    nums = re.findall(r'-?\d+\.\d+', latlng_str)
    if len(nums) >= 2:
        return float(nums[0]), float(nums[1])
    return None, None


def _parse_ts(ts_str):
    if not ts_str:
        return None
    try:
        return datetime.fromisoformat(ts_str).replace(tzinfo=None)
    except Exception:
        return None


def _max_speed_from_path(timeline_path):
    """Calculate the maximum speed (mph) from a timelinePath segment's points."""
    points = []
    for pt in timeline_path:
        latlng = pt.get('point', '')
        ts_str = pt.get('time', '')
        lat, lng = _parse_latlng(latlng)
        ts = _parse_ts(ts_str)
        if lat is not None and ts is not None:
            points.append((ts, lat, lng))

    if len(points) < 2:
        return 0.0

    points.sort(key=lambda x: x[0])
    max_speed = 0.0
    for i in range(1, len(points)):
        t0, lat0, lng0 = points[i - 1]
        t1, lat1, lng1 = points[i]
        dt_hours = (t1 - t0).total_seconds() / 3600.0
        if dt_hours <= 0:
            continue
        dist_miles = _haversine_miles(lat0, lng0, lat1, lng1)
        speed = dist_miles / dt_hours
        if speed > max_speed:
            max_speed = speed

    return max_speed


MIN_FLIGHT_DISTANCE_MILES = 50   # Shorter than this cannot be a flight
MIN_FLIGHT_DURATION_HOURS = 0.5  # Less than 30 minutes cannot be a flight

def _speed_from_activity_segment(activity, seg_start_ts, seg_end_ts):
    """Detect flights from an activity segment's start/end coords and timestamps.

    Flights show up as UNKNOWN_ACTIVITY_TYPE with widely-separated start/end latLng.
    We require at least 50 miles distance to avoid mis-classifying GPS drift as flying
    (short jumps can imply absurdly high speeds due to imprecise timestamps).

    Returns (distance_miles, speed_mph) or (0, 0) if not usable.
    """
    start_latlng = activity.get('start', {}).get('latLng', '')
    end_latlng = activity.get('end', {}).get('latLng', '')
    if not start_latlng or not end_latlng:
        return 0.0, 0.0
    slat, slng = _parse_latlng(start_latlng)
    elat, elng = _parse_latlng(end_latlng)
    if slat is None or elat is None:
        return 0.0, 0.0
    if not seg_start_ts or not seg_end_ts:
        return 0.0, 0.0
    dt_hours = (seg_end_ts - seg_start_ts).total_seconds() / 3600.0
    if dt_hours <= 0:
        return 0.0, 0.0
    dist_miles = _haversine_miles(slat, slng, elat, elng)
    speed = dist_miles / dt_hours
    return dist_miles, speed, dt_hours


class WtImportTimelineWizard(models.TransientModel):
    _name = 'wt.import.timeline.wizard'
    _description = 'Import Google Timeline'

    import_mode = fields.Selection([
        ('upload', 'Upload File'),
        ('server_path', 'Server File Path'),
    ], string='Import Mode', default='upload', required=True)

    timeline_file = fields.Binary(string='Timeline JSON File')
    timeline_filename = fields.Char(string='Filename')
    server_file_path = fields.Char(
        string='Server File Path',
        help='Full path to Timeline.json on the Odoo server (bypasses upload size limits)'
    )
    min_stop_minutes = fields.Integer(
        string='Minimum Stop Duration (minutes)',
        default=5,
    )
    proximity_meters = fields.Integer(
        string='Location Proximity (meters)',
        default=200,
        help='For Timeline Edits.json only: GPS positions within this distance are grouped as one location'
    )
    geocode = fields.Boolean(
        string='Resolve Addresses via OpenStreetMap',
        default=False,
        help='Geocode addresses after import (slow: ~1 req/sec). Use the "Geocode Next 20" action after import instead.'
    )

    def action_import(self):
        self.ensure_one()

        if self.import_mode == 'server_path':
            if not self.server_file_path:
                raise UserError(_('Please enter the server file path.'))
            path = self.server_file_path.strip()
            if not os.path.exists(path):
                raise UserError(_('File not found on server: %s') % path)
            try:
                with open(path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
            except Exception as e:
                raise UserError(_('Error reading file: %s') % str(e))
        else:
            if not self.timeline_file:
                raise UserError(_('Please upload a Timeline JSON file.'))
            try:
                raw = base64.b64decode(self.timeline_file)
                data = json.loads(raw)
            except Exception as e:
                raise UserError(_('Invalid JSON file: %s') % str(e))

        if 'semanticSegments' in data:
            stops = self._parse_semantic_timeline(data)
        elif 'timelineEdits' in data:
            stops = self._parse_raw_timeline(data, self.proximity_meters)
        else:
            raise UserError(_('Unrecognized format. Expected Timeline.json (semanticSegments) or Timeline Edits.json (timelineEdits).'))

        if not stops:
            raise UserError(_('No location stops found in the file.'))

        min_secs = self.min_stop_minutes * 60
        stops = [s for s in stops
                 if s.get('arrived_at') and s.get('departed_at')
                 and (s['departed_at'] - s['arrived_at']).total_seconds() >= min_secs]

        if not stops:
            raise UserError(_('No stops found matching the minimum duration filter.'))

        stops.sort(key=lambda s: s['arrived_at'])

        for i, stop in enumerate(stops):
            if i > 0:
                prev = stops[i - 1]
                stop['distance_from_previous'] = (
                    _haversine_miles(prev['lat'], prev['lng'], stop['lat'], stop['lng'])
                    if prev.get('lat') and stop.get('lat') else 0.0
                )
                travel_delta = stop['arrived_at'] - prev['departed_at']
                stop['travel_time_from_previous'] = max(travel_delta.total_seconds() / 3600, 0.0)
            else:
                stop['distance_from_previous'] = 0.0
                stop['travel_time_from_previous'] = 0.0

        LocationLog = self.env['wt.location.log']
        existing = set(
            r.arrived_at.strftime('%Y-%m-%d %H:%M:%S')
            for r in LocationLog.search([])
            if r.arrived_at
        )

        created_ids = []
        skipped = 0
        for stop in stops:
            arrived_str = stop['arrived_at'].strftime('%Y-%m-%d %H:%M:%S')
            if arrived_str in existing:
                skipped += 1
                continue
            log = LocationLog.create({
                'date': stop['arrived_at'].date(),
                'arrived_at': stop['arrived_at'],
                'departed_at': stop['departed_at'],
                'latitude': stop.get('lat') or 0.0,
                'longitude': stop.get('lng') or 0.0,
                'place_name': stop.get('place_name', ''),
                'category': stop.get('category', ''),
                'travel_mode': stop.get('travel_mode', 'unknown'),
                'distance_from_previous': stop.get('distance_from_previous', 0.0),
                'travel_time_from_previous': stop.get('travel_time_from_previous', 0.0),
                'source': 'google_timeline',
            })
            created_ids.append(log.id)
            existing.add(arrived_str)

        if not created_ids and skipped:
            raise UserError(_('All %d stops already exist. Nothing new to import.') % skipped)

        created = LocationLog.browse(created_ids)
        if self.geocode and created:
            created.action_geocode()

        return {
            'type': 'ir.actions.act_window',
            'name': _('Imported %d new stops (%d skipped as duplicates)') % (len(created_ids), skipped),
            'res_model': 'wt.location.log',
            'view_mode': 'list,form',
            'domain': [('id', 'in', created_ids)],
            'target': 'current',
        }

    def _parse_semantic_timeline(self, data):
        """Parse Timeline.json (semanticSegments format).

        Travel mode priority:
          1. Explicit activity type (if not UNKNOWN)
          2. Speed inferred from activity segment start/end coords + timestamps
             (catches flights that have no timelinePath GPS trail)
          3. Max speed calculated from preceding timelinePath points
          4. 'unknown' as fallback
        """
        stops = []
        segments = data.get('semanticSegments', [])

        pending_mode = 'unknown'
        pending_path_points = []

        for seg in segments:
            # --- timelinePath: collect GPS points for speed inference ---
            if 'timelinePath' in seg:
                pending_path_points.extend(seg['timelinePath'])
                continue

            # --- activity segment ---
            if 'activity' in seg:
                activity = seg['activity']
                top = activity.get('topCandidate', {})
                mode = _activity_type_to_travel_mode(top.get('type', ''))
                if mode:
                    # Explicit type wins (driving, walking, transit, cycling)
                    pending_mode = mode
                else:
                    # UNKNOWN type â€” only infer flying if distance is large enough
                    # to rule out GPS drift (short jumps can look fast due to imprecise timestamps)
                    seg_start = _parse_ts(seg.get('startTime'))
                    seg_end = _parse_ts(seg.get('endTime'))
                    dist, speed, dt_hours = _speed_from_activity_segment(activity, seg_start, seg_end)
                    if (dist >= MIN_FLIGHT_DISTANCE_MILES
                            and speed > 150
                            and dt_hours >= MIN_FLIGHT_DURATION_HOURS):
                        pending_mode = 'flying'
                continue

            # --- visit segment: the stop we record ---
            if 'visit' in seg:
                start_ts = _parse_ts(seg.get('startTime'))
                end_ts = _parse_ts(seg.get('endTime'))
                if not start_ts or not end_ts:
                    pending_mode = 'unknown'
                    pending_path_points = []
                    continue

                candidate = seg['visit'].get('topCandidate', {})
                semantic_type = candidate.get('semanticType', 'UNKNOWN')
                latlng_str = candidate.get('placeLocation', {}).get('latLng', '')
                lat, lng = _parse_latlng(latlng_str) if latlng_str else (None, None)

                category = SEMANTIC_TYPE_CATEGORY.get(
                    semantic_type,
                    semantic_type.replace('_', ' ').title() if semantic_type else 'Unknown'
                )

                # Determine travel mode
                travel_mode = pending_mode
                if travel_mode == 'unknown' and pending_path_points:
                    max_speed = _max_speed_from_path(pending_path_points)
                    travel_mode = _speed_to_travel_mode(max_speed)

                stops.append({
                    'arrived_at': start_ts,
                    'departed_at': end_ts,
                    'lat': lat,
                    'lng': lng,
                    'place_name': '',
                    'category': category,
                    'travel_mode': travel_mode,
                })

                pending_mode = 'unknown'
                pending_path_points = []
                continue

            # timelineMemory or other segment types â€” ignore
            pass

        return stops

    def _parse_raw_timeline(self, data, proximity_meters=200):
        """Parse Timeline Edits.json (raw GPS signal format from Google Takeout)."""
        positions = []
        activities = []
        for entry in data.get('timelineEdits', []):
            raw = entry.get('rawSignal', {}).get('signal', {})
            if 'position' in raw:
                pos = raw['position']
                point = pos.get('point', {})
                lat = point.get('latE7', 0) / 1e7
                lng = point.get('lngE7', 0) / 1e7
                ts_str = pos.get('timestamp', '')
                if ts_str and lat and lng:
                    ts = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
                    positions.append({'ts': ts, 'lat': lat, 'lng': lng})
            elif 'activityRecord' in raw:
                ar = raw['activityRecord']
                ts_str = ar.get('timestamp', '')
                acts = ar.get('detectedActivities', [])
                if ts_str and acts:
                    ts = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
                    dominant = max(acts, key=lambda x: x.get('probability', 0))
                    activities.append({'ts': ts, 'type': dominant['activityType']})

        if not positions:
            return []

        positions.sort(key=lambda x: x['ts'])
        activities.sort(key=lambda x: x['ts'])

        def dominant_mode(start_ts, end_ts):
            window = [a for a in activities if start_ts <= a['ts'] <= end_ts]
            if not window:
                return 'unknown'
            counts = Counter(a['type'] for a in window)
            top_type = counts.most_common(1)[0][0]
            return _activity_type_to_travel_mode(top_type) or 'unknown'

        stops = []
        current_cluster = [positions[0]]
        for pos in positions[1:]:
            prev = current_cluster[-1]
            if _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng']) <= proximity_meters:
                current_cluster.append(pos)
            else:
                avg_lat = sum(p['lat'] for p in current_cluster) / len(current_cluster)
                avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster)
                stops.append({
                    'arrived_at': current_cluster[0]['ts'].replace(tzinfo=None),
                    'departed_at': current_cluster[-1]['ts'].replace(tzinfo=None),
                    'lat': avg_lat, 'lng': avg_lng,
                    'travel_mode': dominant_mode(current_cluster[-1]['ts'], pos['ts']),
                    'category': '', 'place_name': '',
                })
                current_cluster = [pos]

        if current_cluster:
            avg_lat = sum(p['lat'] for p in current_cluster) / len(current_cluster)
            avg_lng = sum(p['lng'] for p in current_cluster) / len(current_cluster)
            stops.append({
                'arrived_at': current_cluster[0]['ts'].replace(tzinfo=None),
                'departed_at': current_cluster[-1]['ts'].replace(tzinfo=None),
                'lat': avg_lat, 'lng': avg_lng,
                'travel_mode': 'unknown', 'category': '', 'place_name': '',
            })

        for i, stop in enumerate(stops):
            if stop['arrived_at'] == stop['departed_at'] and i + 1 < len(stops):
                gap = (stops[i + 1]['arrived_at'] - stop['arrived_at']).total_seconds()
                stop['departed_at'] = stop['arrived_at'] + timedelta(seconds=gap / 2)

        return stops