from __future__ import annotations

import logging
import traceback
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List

from django.core.cache import cache
from django.conf import settings
from django.core.mail import send_mail
from django.utils import timezone as dj_tz
from django.db import transaction
from django.contrib.auth import get_user_model
from bazi.models_group import GroupRelation
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication
from rest_framework_simplejwt.authentication import JWTAuthentication
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, inline_serializer
from rest_framework import serializers

from bazi.models import Person as BaziPerson
from .views import CustomPageNumberPagination


_DESC = """
Return 3-party group relations (三合/三刑) for the logged-in user.

This endpoint manages background processing of complex BaZi relationship calculations
between the logged-in user (owner) and their associated persons.

## Processing States

### "processing" Response
Background task is running or about to start. Returns:
```json
{
  "status": "processing",
  "results": [],
  "count": 0
}
```

### "ready" Response  
Results are available. Returns paginated data with full BaZi person data:
```json
{
  "status": "ready", 
  "results": [
    {
      "t": 0,
      "p1": 123,
      "p2": 456, 
      "by": [
        ["o", "d", "e", 2],
        ["p1", "d", "e", 6], 
        ["p2", "d", "e", 10]
      ],
      "p1_data": {
        "name": "Person 1",
        "gender": "M",
        "birth_date": "1990-01-01",
        "birth_time": "12:00:00",
        "bazi": { "yg": 0, "ye": 2, "mg": 1, "me": 5, "dg": 1, "de": 7, "hg": 5, "he": 9 }
      },
      "p2_data": {
        "name": "Person 2", 
        "gender": "F",
        "birth_date": "1985-05-15",
        "birth_time": "08:30:00",
        "bazi": { "yg": 3, "ye": 1, "mg": 4, "me": 8, "dg": 2, "de": 4, "hg": 6, "he": 0 }
      }
    }
  ],
  "count": 1,
  "next": "http://example.com/api/bazi/person-relations/?page=2",
  "previous": null
}
```

### "error" Response
Previous run exceeded timeout. Returns:
```json
{
  "status": "error",
  "retry_after_seconds": 300,
  "results": [],
  "count": 0
}
```

## Data Structure Details

### Relation Types (`t` field)
- `0`: 三合 (sanhe) - Three Harmony relationship (positive)
- `1`: 三刑 (sanxing) - Three Punishment relationship (negative)

### Person IDs (`p1`, `p2` fields)
- Integer IDs referring to BaZi Person records
- Both persons are associated with the logged-in user
- Relationship involves: Owner + Person1 + Person2

### Formation Details (`by` field)
Array of 3 elements showing how the relationship is formed:
```
[
  ["o", "d", "e", <earth_branch_index>],   // Owner's day pillar earth branch
  ["p1", "d", "e", <earth_branch_index>],  // Person1's day pillar earth branch  
  ["p2", "d", "e", <earth_branch_index>]   // Person2's day pillar earth branch
]
```

**Earth Branch Indices:**
- 0: 子 (zi), 1: 丑 (chou), 2: 寅 (yin), 3: 卯 (mao)
- 4: 辰 (chen), 5: 巳 (si), 6: 午 (wu), 7: 未 (wei)
- 8: 申 (shen), 9: 酉 (you), 10: 戌 (xu), 11: 亥 (hai)

## Example Relationships

### 三合 (Three Harmony) Example
```json
{
  "t": 0,
  "p1": 123,
  "p2": 456,
  "by": [
    ["o", "d", "e", 2],   // Owner: 寅 (yin)
    ["p1", "d", "e", 6],  // Person 123: 午 (wu)  
    ["p2", "d", "e", 10]  // Person 456: 戌 (xu)
  ]
}
```
This represents 寅午戌三合 (yin-wu-xu three harmony forming fire triad).

### 三刑 (Three Punishment) Example
```json
{
  "t": 1, 
  "p1": 789,
  "p2": 101,
  "by": [
    ["o", "d", "e", 2],   // Owner: 寅 (yin)
    ["p1", "d", "e", 5],  // Person 789: 巳 (si)
    ["p2", "d", "e", 8]   // Person 101: 申 (shen)  
  ]
}
```
This represents 寅巳申三刑 (yin-si-shen three punishments).

## Background Processing

- **Automatic Trigger**: Processing starts automatically on first request if state is 'idle'
- **Recalculation**: Triggered when owner's DOB/time changes or persons are added/deleted
- **Timeout**: Dynamic timeout based on number of records (10s per 100 records, minimum 10s)
- **Error Recovery**: Automatic retry after 5 minutes for timed-out operations
- **Admin Notification**: Email sent to TECH_CONTACT on timeout (if configured)

## Pagination

Results are paginated with standard DRF pagination:
- `count`: Total number of group relations
- `next`: URL for next page (if available)
- `previous`: URL for previous page (if available)
- `results`: Array of relation objects for current page

Default page size follows `PAGINATE_BY['default']` setting.

## Complete Examples

**Processing State Response:**
```json
{
  "status": "processing",
  "count": 0,
  "results": []
}
```

**Ready State with Results:**
```json
{
  "status": "ready",
  "count": 2,
  "next": null,
  "previous": null,
  "results": [
    {
      "t": 0,
      "p1": 123,
      "p2": 456,
      "by": [
        ["o", "d", "e", 2],
        ["p1", "d", "e", 6], 
        ["p2", "d", "e", 10]
      ],
      "p1_data": {
        "name": "Person 1",
        "gender": "M",
        "birth_date": "1990-01-01",
        "birth_time": "12:00:00",
        "bazi": { "yg": 0, "ye": 2, "mg": 1, "me": 5, "dg": 1, "de": 7, "hg": 5, "he": 9 }
      },
      "p2_data": {
        "name": "Person 2", 
        "gender": "F",
        "birth_date": "1985-05-15",
        "birth_time": "08:30:00",
        "bazi": { "yg": 3, "ye": 1, "mg": 4, "me": 8, "dg": 2, "de": 4, "hg": 6, "he": 0 }
      }
    },
    {
      "t": 1,
      "p1": 789,
      "p2": 101,
      "by": [
        ["o", "d", "e", 2],
        ["p1", "d", "e", 5],
        ["p2", "d", "e", 8]
      ],
      "p1_data": {
        "name": "Person 3",
        "gender": "M",
        "birth_date": "1985-03-12",
        "birth_time": "14:30:00",
        "bazi": { "yg": 2, "ye": 5, "mg": 3, "me": 2, "dg": 4, "de": 8, "hg": 7, "he": 11 }
      },
      "p2_data": {
        "name": "Person 4",
        "gender": "F",
        "birth_date": "1992-09-20",
        "birth_time": "09:15:00",
        "bazi": { "yg": 8, "ye": 8, "mg": 5, "me": 9, "dg": 9, "de": 3, "hg": 1, "he": 5 }
      }
    }
  ]
}
```

**Error State Response:**
```json
{
  "status": "error",
  "retry_after_seconds": 300,
  "count": 0,
  "results": []
}
```
"""


def _cache_keys(user_id: int) -> Dict[str, str]:
    prefix = f"bazi:grp:{user_id}:"
    return {
        "state": prefix + "state",
        "started_at": prefix + "started_at",
        "updated_at": prefix + "updated_at",
        "error_at": prefix + "error_at",
        "last_retry_at": prefix + "last_retry_at",
        "results": prefix + "results",
        "count": prefix + "count",
    }


def _now_iso() -> str:
    return dj_tz.now().isoformat()


def _spawn_recalc(user_id: int) -> None:
    # Spawn management command in background without blocking
    import os, subprocess, sys
    import logging
    import traceback
    
    # Setup logging for API view
    logger = logging.getLogger('api_person_relations')
    
    try:
        # Get working directory - use current working directory for reliability
        # This is what Django uses by default and should work in both dev and production
        cwd = os.getcwd()
        
        # Debug the path calculation
        logger.info(f"__file__: {__file__}")
        logger.info(f"Current working directory (cwd): {cwd}")
        
        # Verify manage.py exists in current working directory
        if not os.path.exists(os.path.join(cwd, "manage.py")):
            # Try to find manage.py in parent directories
            search_dir = cwd
            for i in range(5):  # Search up to 5 levels up
                parent_dir = os.path.dirname(search_dir)
                if parent_dir == search_dir:  # Reached root
                    break
                search_dir = parent_dir
                if os.path.exists(os.path.join(search_dir, "manage.py")):
                    cwd = search_dir
                    logger.info(f"Found manage.py in parent directory: {cwd}")
                    break
            else:
                # Still not found, log error
                logger.error(f"manage.py not found in current directory or any parent directories")
                logger.error(f"Current cwd: {cwd}")
                logger.error(f"Files in current cwd: {os.listdir(current_cwd)[:10] if os.path.exists(cwd) else 'N/A'}")
                raise FileNotFoundError(f"manage.py not found in {cwd} or any parent directories")
        
        # Verify manage.py exists
        manage_path = os.path.join(cwd, "manage.py")
        if not os.path.isfile(manage_path):
            error_msg = f"manage.py not found at {manage_path}"
            logger.error(error_msg)
            raise FileNotFoundError(error_msg)
        
        # Set environment variables - critical for uWSGI subprocesses
        env = os.environ.copy()
        
        # Ensure critical environment variables are set
        env["DJANGO_ENV"] = env.get("DJANGO_ENV", "development")
        env["PYTHONPATH"] = cwd  # Ensure Python can find the project
        env["DJANGO_SETTINGS_MODULE"] = "iching.settings"  # Explicitly set Django settings
        
        # Log environment for debugging
        logger.info(f"Environment: DJANGO_ENV={env.get('DJANGO_ENV')}, PYTHONPATH={env.get('PYTHONPATH')}")
        logger.info(f"Current working directory: {os.getcwd()}")
        logger.info(f"Target working directory: {cwd}")
        
        logger.info(f"Spawning background recalculation for user {user_id}")
        logger.info(f"Working directory: {cwd}")
        logger.info(f"manage.py path: {manage_path}")
        logger.info(f"Python executable: {sys.executable}")
        logger.info(f"Using Django call_command approach for user {user_id}")
        
        # Use Django's built-in management command execution to ensure proper context
        try:
            from django.core.management import call_command
            from django.core.management.base import CommandError
            import threading
            import time
            
            def run_management_command():
                """Run the management command in a separate thread with proper Django context"""
                try:
                    # Set the user state to processing
                    from django.contrib.auth import get_user_model
                    User = get_user_model()
                    try:
                        usr = User.objects.get(id=user_id)
                        usr.group_relations_state = 'processing'
                        usr.group_relations_started_at = dj_tz.now()
                        usr.save(update_fields=['group_relations_state', 'group_relations_started_at'])
                    except User.DoesNotExist:
                        logger.error(f"User {user_id} not found")
                        return
                    
                    logger.info(f"Starting management command execution for user {user_id}")
                    
                    # Execute the management command directly
                    call_command('recalc_bazi_relations', user=user_id, verbosity=1)
                    
                    logger.info(f"Management command completed successfully for user {user_id}")
                    
                    # Update user state to completed
                    try:
                        usr.refresh_from_db()
                        usr.group_relations_state = 'completed'
                        usr.group_relations_updated_at = dj_tz.now()
                        usr.save(update_fields=['group_relations_state', 'group_relations_updated_at'])
                    except User.DoesNotExist:
                        pass
                        
                except CommandError as e:
                    logger.error(f"Management command failed for user {user_id}: {str(e)}")
                    # Update user state to error
                    try:
                        usr.refresh_from_db()
                        usr.group_relations_state = 'error'
                        usr.group_relations_error_at = dj_tz.now()
                        usr.save(update_fields=['group_relations_state', 'group_relations_error_at'])
                    except User.DoesNotExist:
                        pass
                except Exception as e:
                    logger.error(f"Unexpected error in management command for user {user_id}: {str(e)}")
                    # Update user state to error
                    try:
                        usr.refresh_from_db()
                        usr.group_relations_state = 'error'
                        usr.group_relations_error_at = dj_tz.now()
                        usr.save(update_fields=['group_relations_state', 'group_relations_error_at'])
                    except User.DoesNotExist:
                        pass
            
            # Run the management command in a background thread
            thread = threading.Thread(target=run_management_command, daemon=True)
            thread.start()
            
            # Create a dummy process object for compatibility
            class DummyProcess:
                def __init__(self, pid):
                    self.pid = pid
                def poll(self):
                    return None  # Always running
                def communicate(self, timeout=None):
                    return (b"", b"")  # Empty output
            
            process = DummyProcess(os.getpid())
            
        except Exception as e:
            logger.error(f"Failed to start management command for user {user_id}: {str(e)}")
            raise
        
        logger.info(f"Background process spawned successfully for user {user_id} (PID: {process.pid})")
        
        # For uWSGI environments, always capture output after a brief delay
        # This ensures we get logs even if the process crashes early
        import time
        time.sleep(0.5)  # Brief delay to let process start
        
        # Check process status and capture output
        if process.poll() is None:
            logger.info(f"Process {process.pid} is running successfully")
            # In uWSGI, we need to detach completely
            # The process will continue running independently
        else:
            # Process exited immediately - capture output for debugging
            logger.error(f"Process {process.pid} exited immediately with code: {process.returncode}")
            try:
                stdout, stderr = process.communicate(timeout=5)
                if stderr:
                    error_msg = f"[recalc_bazi_relations:{user_id}] stderr:\n{stderr.decode('utf-8', errors='ignore')}"
                    logger.error(error_msg)
                    print(f"ERROR: {error_msg}")  # Console output for debugging
                if stdout:
                    info_msg = f"[recalc_bazi_relations:{user_id}] stdout:\n{stdout.decode('utf-8', errors='ignore')}"
                    logger.info(info_msg)
                    print(f"INFO: {info_msg}")  # Console output for debugging
            except subprocess.TimeoutExpired:
                logger.error(f"Timeout waiting for process {process.pid} output")
                process.kill()
                stdout, stderr = process.communicate()
                if stderr:
                    logger.error(f"[recalc_bazi_relations:{user_id}] final stderr:\n{stderr.decode('utf-8', errors='ignore')}")
        
    except Exception as e:
        logger.error(f"Failed to spawn background process for user {user_id}: {str(e)}")
        logger.error(f"Error traceback: {traceback.format_exc()}")
        raise


def _get_timeout_seconds(num_records: int) -> int:
    # 10 seconds per 100 records, minimum 10s
    blocks = max(1, (num_records + 99) // 100)
    return blocks * 10


class PersonRelationsAPIView(APIView):
    authentication_classes = [SessionAuthentication, JWTAuthentication]
    permission_classes = [IsAuthenticated]

    @extend_schema(
        description=_DESC,
        parameters=[
            OpenApiParameter(
                name='page', 
                type=OpenApiTypes.INT, 
                location=OpenApiParameter.QUERY, 
                required=False,
                description='Page number for pagination (default: 1)'
            ),
            OpenApiParameter(
                name='page_size', 
                type=OpenApiTypes.INT, 
                location=OpenApiParameter.QUERY, 
                required=False,
                description='Number of results per page (default: follows PAGINATE_BY setting)'
            ),
        ],
        responses={
            200: inline_serializer(
                name='PersonRelationsResponse',
                fields={
                    'status': serializers.ChoiceField(
                        choices=['processing', 'ready', 'error'],
                        help_text='Current processing state of group relations calculation'
                    ),
                    'count': serializers.IntegerField(
                        required=False,
                        help_text='Total number of group relations found'
                    ),
                    'next': serializers.URLField(
                        required=False, 
                        allow_null=True,
                        help_text='URL for next page of results (if available)'
                    ),
                    'previous': serializers.URLField(
                        required=False, 
                        allow_null=True,
                        help_text='URL for previous page of results (if available)'
                    ),
                    'results': serializers.ListField(
                        child=inline_serializer(
                            name='GroupRelation',
                            fields={
                                't': serializers.ChoiceField(
                                    choices=[0, 1],
                                    help_text='Relation type: 0=三合(sanhe/harmony), 1=三刑(sanxing/punishment)'
                                ),
                                'p1': serializers.IntegerField(
                                    help_text='Person ID for first participant in the relationship'
                                ),
                                'p2': serializers.IntegerField(
                                    help_text='Person ID for second participant in the relationship'
                                ),
                                'by': serializers.ListField(
                                    child=serializers.ListField(
                                        child=serializers.CharField(),
                                        min_length=4,
                                        max_length=4
                                    ),
                                    min_length=3,
                                    max_length=3,
                                    help_text='Formation details: [["o","d","e",<earth_index>], ["p1","d","e",<earth_index>], ["p2","d","e",<earth_index>]]'
                                ),
                                'p1_data': serializers.DictField(
                                    required=False,
                                    help_text='Essential person data: {name, gender, birth_date, birth_time, bazi: {yg, ye, mg, me, dg, de, hg, he}}'
                                ),
                                'p2_data': serializers.DictField(
                                    required=False,
                                    help_text='Essential person data: {name, gender, birth_date, birth_time, bazi: {yg, ye, mg, me, dg, de, hg, he}}'
                                ),
                            }
                        ),
                        required=False,
                        help_text='Array of group relation objects for current page'
                    ),
                    'retry_after_seconds': serializers.IntegerField(
                        required=False,
                        help_text='Seconds to wait before retry (only present when status="error")'
                    ),
                }
            ),
            401: inline_serializer(
                name='UnauthorizedResponse',
                fields={
                    'detail': serializers.CharField(default='Authentication credentials were not provided.')
                }
            ),
        },

    )
    def get(self, request):
        user = request.user
        keys = _cache_keys(user.id)
        
        # Setup logging
        logger = logging.getLogger('api_person_relations')
        logger.info(f"API request from user {user.id} for person relations")

        # If no owner person, return ready with empty
        owner = (
            BaziPerson.objects.filter(created_by=user, owner=True)
            .order_by("-created_at")
            .first()
        )
        if owner is None:
            return Response({
                "status": "ready",
                "pagination": {"page": 1, "page_size": 0, "total": 0},
                "results": [],
            })

        # Determine number of records for timeout estimation
        num_records = BaziPerson.objects.filter(created_by=user).count()
        timeout_seconds = _get_timeout_seconds(num_records)

        # Read current state from DB (main.User)
        User = get_user_model()
        usr = User.objects.get(id=user.id)
        state = usr.group_relations_state or "idle"
        started_at = usr.group_relations_started_at.isoformat() if usr.group_relations_started_at else None
        updated_at = usr.group_relations_updated_at.isoformat() if usr.group_relations_updated_at else None
        error_at = usr.group_relations_error_at.isoformat() if usr.group_relations_error_at else None

        # If processing too long -> error
        if state == "processing" and started_at:
            try:
                started_dt = datetime.fromisoformat(started_at)
            except Exception:
                started_dt = dj_tz.now() - timedelta(seconds=timeout_seconds + 1)
            elapsed = (dj_tz.now() - started_dt).total_seconds()
            if elapsed > timeout_seconds:
                usr.group_relations_state = "error"
                usr.group_relations_error_at = dj_tz.now()
                usr.save(update_fields=["group_relations_state", "group_relations_error_at"])
                state = "error"
                # Notify admin/tech contact
                try:
                    to_email = getattr(settings, "TECH_CONTACT", None) or getattr(settings, "CONTACT_EMAIL", None)
                    if to_email:
                        send_mail(
                            subject="BaZi group relations timeout",
                            message=f"User {user.id} group relations processing exceeded timeout ({timeout_seconds}s).",
                            from_email=None,
                            recipient_list=[to_email],
                            fail_silently=True,
                        )
                except Exception:
                    pass

        # Handle states
        if state == "idle":
            # Transition to processing, spawn job
            logger.info(f"User {user.id} state: idle -> processing, spawning background job")
            usr.group_relations_state = "processing"
            usr.group_relations_started_at = dj_tz.now()
            usr.save(update_fields=["group_relations_state", "group_relations_started_at"])
            transaction.on_commit(lambda: _spawn_recalc(user.id))
            return Response({"status": "processing"})

        if state == "processing":
            return Response({"status": "processing"})

        if state == "error":
            # If at least 5 minutes passed since error, retry
            retry_after = 300
            if error_at:
                try:
                    err_dt = datetime.fromisoformat(error_at)
                    remain = 300 - int((dj_tz.now() - err_dt).total_seconds())
                    if remain <= 0:
                        logger.info(f"User {user.id} state: error -> processing (retry), spawning background job")
                        usr.group_relations_state = "processing"
                        usr.group_relations_started_at = dj_tz.now()
                        usr.save(update_fields=["group_relations_state", "group_relations_started_at"])
                        transaction.on_commit(lambda: _spawn_recalc(user.id))
                        return Response({"status": "processing"})
                    logger.info(f"User {user.id} state: error, retry in {max(1, remain)} seconds")
                    return Response({"status": "error", "retry_after_seconds": max(1, remain)})
                except Exception:
                    logger.warning(f"User {user.id} error parsing error timestamp: {error_at}")
                    pass
            logger.info(f"User {user.id} state: error, retry in {retry_after} seconds")
            return Response({"status": "error", "retry_after_seconds": retry_after})

        # Completed/ready
        logger.info(f"User {user.id} state: ready, returning {len(GroupRelation.objects.filter(owner_user_id=user.id))} relations")
        # Build from DB rows
        rows = GroupRelation.objects.filter(owner_user_id=user.id).order_by("relation_type", "id")
        
        # Collect all person IDs we need to fetch
        person_ids = set()
        for r in rows:
            person_ids.add(r.person1_id)
            person_ids.add(r.person2_id)
        
        # Fetch all persons with their BaZi data in a single query
        persons_data = {}
        if person_ids:
            persons = BaziPerson.objects.filter(id__in=person_ids, created_by=user)
            for p in persons:
                # Create simplified person data with only essential fields
                person_data = {
                    "name": p.name,
                    "gender": p.gender,
                    "birth_date": p.birth_date,
                    "birth_time": p.birth_time,
                }
                
                # Add simplified BaZi result (4 pillars only)
                if p.bazi_result:
                    bazi_simplified = {}
                    for pillar_name, pillar_key in [("y", "year"), ("m", "month"), ("d", "day"), ("h", "hour")]:
                        pillar_data = p.bazi_result.get(pillar_key, {})
                        if pillar_data:
                            bazi_simplified[f"{pillar_name}g"] = pillar_data.get("god")
                            bazi_simplified[f"{pillar_name}e"] = pillar_data.get("earth")
                    
                    person_data["bazi"] = bazi_simplified
                
                persons_data[p.id] = person_data
        
        results: List[Dict[str, Any]] = []
        for r in rows:
            t = 0 if r.relation_type == 'sanhe' else 1
            result = {
                "t": t,
                "p1": r.person1_id,
                "p2": r.person2_id,
                "by": r.by or [],
            }
            
            # Add full person data for p1 and p2
            if r.person1_id in persons_data:
                result["p1_data"] = persons_data[r.person1_id]
            if r.person2_id in persons_data:
                result["p2_data"] = persons_data[r.person2_id]
                
            results.append(result)
        total = len(results)

        # Manual pagination to include top-level status
        paginator = CustomPageNumberPagination()
        try:
            page = int(request.query_params.get("page", 1))
        except Exception:
            page = 1
        page_size = paginator.get_page_size(request)
        start = max(0, (page - 1) * page_size)
        end = start + page_size
        sliced = results[start:end]
        return Response({
            "status": "ready",
            "results": sliced,
            "pagination": {
                "page": page,
                "page_size": page_size,
                "total": total,
            }
        })


