from odoo import models, fields, api, _
from odoo.exceptions import UserError
import json
import base64
import bisect
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'}

CATEGORY_LABELS = {
    'IN_PASSENGER_VEHICLE': 'In Vehicle',
    'IN_VEHICLE': 'In Vehicle',
    'IN_ROAD_VEHICLE': 'In Vehicle',
    'IN_RAIL_VEHICLE': 'Rail / Transit',
    'IN_TWO_WHEELER_VEHICLE': 'Motorcycle / Scooter',
    'WALKING': 'Walking',
    'ON_FOOT': 'Walking',
    'RUNNING': 'Running',
    'ON_BICYCLE': 'Cycling',
    'STILL': 'Stationary',
    'UNKNOWN': 'Unknown',
    'EXITING_VEHICLE': 'In Vehicle',
    'TILTING': 'Unknown',
}

PROXIMITY_METERS = 200


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 _get_travel_mode(activity_type):
    if activity_type in VEHICLE_ACTIVITIES:
        return 'driving'
    if activity_type in WALKING_ACTIVITIES:
        return 'walking'
    if activity_type in CYCLING_ACTIVITIES:
        return 'cycling'
    return 'unknown'


def _dominant_activity(activities, start_ts, end_ts):
    """Get dominant activity type between two timestamps."""
    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)
    return counts.most_common(1)[0][0]


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

    timeline_file = fields.Binary(string='Timeline JSON File', required=True)
    timeline_filename = fields.Char(string='Filename')
    min_stop_minutes = fields.Integer(
        string='Minimum Stop Duration (minutes)',
        default=5,
        help='Ignore stops shorter than this duration'
    )
    proximity_meters = fields.Integer(
        string='Location Proximity (meters)',
        default=200,
        help='GPS positions within this distance are grouped as the same location'
    )
    geocode = fields.Boolean(
        string='Resolve Addresses via OpenStreetMap',
        default=True,
    )

    def action_import(self):
        self.ensure_one()
        try:
            raw = base64.b64decode(self.timeline_file)
            data = json.loads(raw)
        except Exception as e:
            raise UserError(_('Invalid JSON file: %s') % str(e))

        stops = self._parse_timeline(data, self.proximity_meters)
        if not stops:
            raise UserError(_('No location stops found in the uploaded file.'))

        # Filter by minimum stop duration
        min_secs = self.min_stop_minutes * 60
        stops = [s for s in stops
                 if (s['departed_at'] - s['arrived_at']).total_seconds() >= min_secs]

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

        # Compute distances and travel times between consecutive stops
        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']
                )
                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

        # Skip duplicates
        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 = stop['arrived_at'].replace(tzinfo=None)
            departed = stop['departed_at'].replace(tzinfo=None)
            arrived_str = arrived.strftime('%Y-%m-%d %H:%M:%S')

            if arrived_str in existing:
                skipped += 1
                continue

            log = LocationLog.create({
                'date': arrived.date(),
                'arrived_at': arrived,
                'departed_at': departed,
                'latitude': stop['lat'],
                'longitude': stop['lng'],
                'travel_mode': stop.get('travel_mode', 'unknown'),
                'category': stop.get('category', ''),
                'distance_from_previous': stop['distance_from_previous'],
                'travel_time_from_previous': stop['travel_time_from_previous'],
                '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:
            created.action_geocode()

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

    def _parse_timeline(self, data, proximity_meters=200):
        """
        Parse Google Timeline Edits JSON into location stops.
        Positions represent stationary moments — cluster by proximity.
        Activity records between clusters give travel category/mode.
        """
        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'])

        # Cluster consecutive positions within proximity_meters
        stops = []
        current_cluster = [positions[0]]

        for pos in positions[1:]:
            prev = current_cluster[-1]
            dist = _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng'])

            if dist <= 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)

                # Activity between this stop and next = travel mode
                act_type = _dominant_activity(
                    activities, current_cluster[-1]['ts'], pos['ts']
                )
                travel_mode = _get_travel_mode(act_type)
                category = CATEGORY_LABELS.get(act_type, act_type.replace('_', ' ').title())

                stops.append({
                    'arrived_at': current_cluster[0]['ts'],
                    'departed_at': current_cluster[-1]['ts'],
                    'lat': avg_lat,
                    'lng': avg_lng,
                    'travel_mode': travel_mode,
                    'category': category,
                })
                current_cluster = [pos]

        # Last cluster
        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'],
                'departed_at': current_cluster[-1]['ts'],
                'lat': avg_lat,
                'lng': avg_lng,
                'travel_mode': 'unknown',
                'category': '',
            })

        # Estimate duration for single-position stops
        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
