from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional

from pydantic import BaseModel

STEP_LOG_KEY = "gremlin:steps:{conversation_id}"
STEP_LOG_TTL = 86400  # 24 hours
UNDO_KEY = "gremlin:undo:{snapshot_id}"
UNDO_TTL = 86400  # 24 hours


class StepStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    UNDONE = "undone"


class StepLogEntry(BaseModel):
    step_id: str
    conversation_id: str
    action_type: str
    label: str
    description: str
    affected_range: Optional[str] = None
    row_count: int = 0
    status: StepStatus = StepStatus.PENDING
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    duration_ms: Optional[int] = None
    undo_snapshot_id: Optional[str] = None
    error_message: Optional[str] = None
    engine_job_id: Optional[str] = None


class UndoSnapshot(BaseModel):
    snapshot_id: str
    step_id: Optional[str] = None
    sheet_name: str
    range_a1: str
    values: List[List[Any]]
    formats: Optional[List[List[Any]]] = None
    created_at: datetime
    expires_at: datetime


async def _get_redis(redis=None):
    if redis is not None:
        return redis
    from ..api.progress import get_redis

    return await get_redis()


async def append_step(
    conversation_id: str,
    entry: StepLogEntry,
    redis=None,
    max_items: int = 100,
) -> None:
    if not conversation_id:
        return
    redis_conn = await _get_redis(redis)
    if not redis_conn:
        return
    key = STEP_LOG_KEY.format(conversation_id=conversation_id)
    await redis_conn.rpush(key, entry.model_dump_json())
    await redis_conn.ltrim(key, -max_items, -1)
    await redis_conn.expire(key, STEP_LOG_TTL)


async def list_steps(
    conversation_id: str,
    redis=None,
) -> List[StepLogEntry]:
    if not conversation_id:
        return []
    redis_conn = await _get_redis(redis)
    if not redis_conn:
        return []
    key = STEP_LOG_KEY.format(conversation_id=conversation_id)
    items = await redis_conn.lrange(key, 0, -1)
    steps: List[StepLogEntry] = []
    for item in items or []:
        if isinstance(item, bytes):
            item = item.decode("utf-8")
        try:
            steps.append(StepLogEntry.model_validate_json(item))
        except Exception:
            continue
    return steps


async def update_step(
    conversation_id: str,
    step_id: str,
    updates: Dict[str, Any],
    redis=None,
    max_items: int = 100,
) -> Optional[StepLogEntry]:
    if not conversation_id or not step_id:
        return None
    redis_conn = await _get_redis(redis)
    if not redis_conn:
        return None
    key = STEP_LOG_KEY.format(conversation_id=conversation_id)
    items = await redis_conn.lrange(key, 0, -1)
    if not items:
        return None

    updated = None
    new_items: List[str] = []
    for item in items:
        raw = item.decode("utf-8") if isinstance(item, bytes) else item
        try:
            entry = StepLogEntry.model_validate_json(raw)
        except Exception:
            continue
        if entry.step_id == step_id:
            payload = entry.model_dump()
            payload.update(updates or {})
            updated = StepLogEntry(**payload)
            new_items.append(updated.model_dump_json())
        else:
            new_items.append(entry.model_dump_json())

    if updated is None:
        return None

    await redis_conn.delete(key)
    if new_items:
        await redis_conn.rpush(key, *new_items[-max_items:])
        await redis_conn.expire(key, STEP_LOG_TTL)
    return updated
