#!/usr/bin/env python3
"""
I2C Core - vzug-e-hinge
=======================
Generic I2C bus management with reading/writing and continuous logging.

Features:
- Bus scanning (detect available /dev/i2c-*)
- Device scanning (like i2cdetect)
- Read/write operations (byte or block)
- Continuous logger thread (background polling)
- Circular buffer for logger data
- Timestamp synchronization (inject clock)
- Thread-safe operations
- Sensor-agnostic (generic register access)

Author: Kynsight
Version: 1.0.0
"""

from __future__ import annotations
import os
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Callable, Tuple, List

try:
    import smbus2
except ImportError:
    smbus2 = None

from buffer_kit.circular_buffer import (
    Status as BufferStatus,
    CircularBufferHandle,
    cb_init,
    cb_write,
    cb_capacity,
    cb_fill_bytes,
    cb_fill_pct,
    cb_overflows,
)


# =============================================================================
# Status & Configuration
# =============================================================================

class Status(Enum):
    """Operation status codes."""
    OK = 0
    ERROR = 1
    BUS_NOT_FOUND = 2
    DEVICE_NOT_FOUND = 3
    ALREADY_OPEN = 4
    NOT_OPEN = 5
    LOGGER_RUNNING = 6
    NACK = 7  # Device did not acknowledge


@dataclass(frozen=True)
class I2CConfig:
    """
    I2C bus configuration.

    bus_id: Bus number (e.g., 1 for /dev/i2c-1)
    buffer_size: Logger circular buffer capacity
    timestamp_source: Optional external time function (for sync with UART)
    """
    bus_id: int
    buffer_size: int = 40 * 1024 * 1024  # 40MB default buffer
    timestamp_source: Optional[Callable[[], float]] = None


@dataclass
class I2CReading:
    """
    Single I2C read/write operation record.
    
    reading_id: Sequential ID (per session)
    timestamp: When operation occurred
    address: Device address (7-bit)
    register: Register address
    data: Raw bytes read/written
    operation: "READ" or "WRITE"
    """
    reading_id: int
    timestamp: float
    address: int
    register: int
    data: bytes
    operation: str  # "READ" or "WRITE"


@dataclass
class BusInfo:
    """
    Information about I2C bus status.
    
    is_open: Bus is opened
    is_logger_running: Logger thread active
    buffer_capacity: Buffer size
    buffer_fill_bytes: Current buffer usage
    buffer_fill_percent: Fill percentage
    buffer_overflows: Overflow count
    total_reads: Total read operations
    total_writes: Total write operations
    total_errors: Total errors encountered
    """
    is_open: bool
    is_logger_running: bool
    buffer_capacity: int
    buffer_fill_bytes: int
    buffer_fill_percent: int
    buffer_overflows: int
    total_reads: int
    total_writes: int
    total_errors: int


# =============================================================================
# I2C Handle
# =============================================================================

@dataclass
class I2CHandle:
    """
    I2C bus handle (internal state - use API functions).
    
    Do not access fields directly - use i2c_* functions.
    """
    config: I2CConfig
    
    # SMBus
    _bus: Optional[smbus2.SMBus] = None
    _is_open: bool = False
    
    # Circular buffer for logger
    _buffer: Optional[CircularBufferHandle] = None
    
    # Logger thread
    _logger_thread: Optional[threading.Thread] = None
    _logger_running: bool = False
    _stop_logger_event: threading.Event = field(default_factory=threading.Event)
    
    # Logger configuration
    _logger_addr: int = 0
    _logger_reg: int = 0
    _logger_len: int = 1
    _logger_interval_ms: int = 100
    
    # Thread safety
    _lock: threading.RLock = field(default_factory=threading.RLock)
    
    # Statistics
    _total_reads: int = 0
    _total_writes: int = 0
    _total_errors: int = 0
    _reading_counter: int = 0
    
    # Timestamp function
    _get_timestamp: Callable[[], float] = field(default=time.perf_counter)
    
    def __post_init__(self):
        """Initialize timestamp source."""
        if self.config.timestamp_source:
            self._get_timestamp = self.config.timestamp_source


# =============================================================================
# Bus Discovery
# =============================================================================

def i2c_scan_buses() -> List[int]:
    """
    Scan for available I2C buses.
    
    Returns:
        List of bus IDs (e.g., [0, 1, 2] for /dev/i2c-0, /dev/i2c-1, etc.)
    """
    buses = []
    try:
        for entry in os.listdir("/dev"):
            if entry.startswith("i2c-"):
                try:
                    bus_id = int(entry.split("-")[1])
                    buses.append(bus_id)
                except ValueError:
                    continue
    except Exception:
        pass
    
    return sorted(buses)


# =============================================================================
# Bus Lifecycle
# =============================================================================

def i2c_create(config: I2CConfig) -> Tuple[Status, Optional[I2CHandle]]:
    """
    Create I2C handle.
    
    Args:
        config: I2C configuration
    
    Returns:
        (Status.OK, handle) on success
        (Status.ERROR, None) on failure
    """
    if not smbus2:
        return (Status.ERROR, None)
    
    if config.bus_id < 0:
        return (Status.ERROR, None)
    
    handle = I2CHandle(config=config)
    return (Status.OK, handle)


def i2c_open(handle: I2CHandle) -> Status:
    """
    Open I2C bus.
    
    Args:
        handle: I2C handle
    
    Returns:
        Status.OK on success
        Status.ALREADY_OPEN if already open
        Status.BUS_NOT_FOUND if bus not available
    """
    if handle._is_open:
        return Status.ALREADY_OPEN
    
    try:
        # Open SMBus
        handle._bus = smbus2.SMBus(handle.config.bus_id)
        
        # Create buffer for logger
        status, buffer = cb_init(handle.config.buffer_size)
        if status != BufferStatus.OK:
            handle._bus.close()
            return Status.ERROR
        
        handle._buffer = buffer
        handle._is_open = True
        
        # Reset statistics
        with handle._lock:
            handle._total_reads = 0
            handle._total_writes = 0
            handle._total_errors = 0
            handle._reading_counter = 0
        
        return Status.OK
    
    except FileNotFoundError:
        return Status.BUS_NOT_FOUND
    except Exception:
        return Status.ERROR


def i2c_close(handle: I2CHandle) -> Status:
    """
    Close I2C bus and cleanup.
    
    Stops logger if running.
    
    Args:
        handle: I2C handle
    
    Returns:
        Status.OK
    """
    if not handle._is_open:
        return Status.OK
    
    # Stop logger first
    if handle._logger_running:
        i2c_stop_logger(handle)
    
    # Close bus
    with handle._lock:
        if handle._bus:
            try:
                handle._bus.close()
            except:
                pass
            handle._bus = None
    
    handle._buffer = None
    handle._is_open = False
    
    return Status.OK


# =============================================================================
# Device Scanning
# =============================================================================

def i2c_scan_devices(handle: I2CHandle) -> Tuple[Status, List[int]]:
    """
    Scan bus for devices (like i2cdetect).
    
    Probes addresses 0x03 to 0x77 (valid 7-bit range).
    
    Args:
        handle: I2C handle (must be open)
    
    Returns:
        (Status.OK, [addresses]) on success
        (Status.NOT_OPEN, []) if bus not open
    """
    if not handle._is_open or not handle._bus:
        return (Status.NOT_OPEN, [])
    
    devices = []
    
    with handle._lock:
        for addr in range(0x03, 0x78):  # Valid 7-bit address range
            try:
                # Quick write to probe device
                handle._bus.write_quick(addr)
                devices.append(addr)
            except OSError:
                # Device not present (NACK)
                continue
            except Exception:
                # Other error - skip
                continue
    
    return (Status.OK, devices)


# =============================================================================
# Read Operations
# =============================================================================

def i2c_read_byte(handle: I2CHandle, addr: int, reg: int) -> Tuple[Status, int]:
    """
    Read single byte from register.
    
    Args:
        handle: I2C handle
        addr: Device address (7-bit)
        reg: Register address
    
    Returns:
        (Status.OK, value) on success
        (Status.NOT_OPEN, 0) if bus not open
        (Status.NACK, 0) if device did not respond
        (Status.ERROR, 0) on other error
    """
    if not handle._is_open or not handle._bus:
        return (Status.NOT_OPEN, 0)
    
    try:
        with handle._lock:
            value = handle._bus.read_byte_data(addr, reg)
            handle._total_reads += 1
        
        return (Status.OK, value)
    
    except OSError as e:
        with handle._lock:
            handle._total_errors += 1
        # errno 121 = Remote I/O error (NACK)
        if e.errno == 121:
            return (Status.NACK, 0)
        return (Status.ERROR, 0)
    
    except Exception:
        with handle._lock:
            handle._total_errors += 1
        return (Status.ERROR, 0)


def i2c_read_block(handle: I2CHandle, addr: int, reg: int, length: int) -> Tuple[Status, bytes]:
    """
    Read block of bytes from register.
    
    Args:
        handle: I2C handle
        addr: Device address (7-bit)
        reg: Register address
        length: Number of bytes to read (1-32)
    
    Returns:
        (Status.OK, data) on success
        (Status.NOT_OPEN, b"") if bus not open
        (Status.NACK, b"") if device did not respond
        (Status.ERROR, b"") on other error
    """
    if not handle._is_open or not handle._bus:
        return (Status.NOT_OPEN, b"")
    
    if length < 1 or length > 32:
        return (Status.ERROR, b"")
    
    try:
        with handle._lock:
            data = handle._bus.read_i2c_block_data(addr, reg, length)
            handle._total_reads += 1
        
        return (Status.OK, bytes(data))
    
    except OSError as e:
        with handle._lock:
            handle._total_errors += 1
        if e.errno == 121:
            return (Status.NACK, b"")
        return (Status.ERROR, b"")
    
    except Exception:
        with handle._lock:
            handle._total_errors += 1
        return (Status.ERROR, b"")


# =============================================================================
# Write Operations
# =============================================================================

def i2c_write_byte(handle: I2CHandle, addr: int, reg: int, value: int) -> Status:
    """
    Write single byte to register.
    
    Args:
        handle: I2C handle
        addr: Device address (7-bit)
        reg: Register address
        value: Byte value (0-255)
    
    Returns:
        Status.OK on success
        Status.NOT_OPEN if bus not open
        Status.NACK if device did not respond
        Status.ERROR on other error
    """
    if not handle._is_open or not handle._bus:
        return Status.NOT_OPEN
    
    if value < 0 or value > 255:
        return Status.ERROR
    
    try:
        with handle._lock:
            handle._bus.write_byte_data(addr, reg, value)
            handle._total_writes += 1
        
        return Status.OK
    
    except OSError as e:
        with handle._lock:
            handle._total_errors += 1
        if e.errno == 121:
            return Status.NACK
        return Status.ERROR
    
    except Exception:
        with handle._lock:
            handle._total_errors += 1
        return Status.ERROR


def i2c_write_block(handle: I2CHandle, addr: int, reg: int, data: bytes) -> Status:
    """
    Write block of bytes to register.
    
    Args:
        handle: I2C handle
        addr: Device address (7-bit)
        reg: Register address
        data: Bytes to write (1-32 bytes)
    
    Returns:
        Status.OK on success
        Status.NOT_OPEN if bus not open
        Status.NACK if device did not respond
        Status.ERROR on other error
    """
    if not handle._is_open or not handle._bus:
        return Status.NOT_OPEN
    
    if len(data) < 1 or len(data) > 32:
        return Status.ERROR
    
    try:
        with handle._lock:
            handle._bus.write_i2c_block_data(addr, reg, list(data))
            handle._total_writes += 1
        
        return Status.OK
    
    except OSError as e:
        with handle._lock:
            handle._total_errors += 1
        if e.errno == 121:
            return Status.NACK
        return Status.ERROR
    
    except Exception:
        with handle._lock:
            handle._total_errors += 1
        return Status.ERROR


# =============================================================================
# Logger (Background Polling)
# =============================================================================

def i2c_start_logger(handle: I2CHandle, addr: int, reg: int, length: int, 
                     interval_ms: int) -> Status:
    """
    Start background logger thread.
    
    Continuously polls register at specified rate and stores in buffer.
    
    Args:
        handle: I2C handle
        addr: Device address (7-bit)
        reg: Register address
        length: Bytes to read (1-32)
        interval_ms: Polling interval in milliseconds
    
    Returns:
        Status.OK on success
        Status.NOT_OPEN if bus not open
        Status.LOGGER_RUNNING if already running
    """
    if not handle._is_open:
        return Status.NOT_OPEN
    
    if handle._logger_running:
        return Status.LOGGER_RUNNING
    
    if length < 1 or length > 32 or interval_ms < 1:
        return Status.ERROR
    
    # Configure logger
    handle._logger_addr = addr
    handle._logger_reg = reg
    handle._logger_len = length
    handle._logger_interval_ms = interval_ms
    
    # Start thread
    handle._stop_logger_event.clear()
    handle._logger_running = True
    
    handle._logger_thread = threading.Thread(
        target=_logger_thread_func,
        args=(handle,),
        daemon=True,
        name=f"I2C-Logger-{handle.config.bus_id}"
    )
    handle._logger_thread.start()
    
    return Status.OK


def i2c_stop_logger(handle: I2CHandle) -> Status:
    """
    Stop background logger thread.
    
    Blocks until thread exits (with timeout).
    
    Args:
        handle: I2C handle
    
    Returns:
        Status.OK
    """
    if not handle._logger_running:
        return Status.OK
    
    handle._stop_logger_event.set()
    
    if handle._logger_thread:
        handle._logger_thread.join(timeout=2.0)
        handle._logger_thread = None
    
    handle._logger_running = False
    
    return Status.OK


def _logger_thread_func(handle: I2CHandle):
    """
    Logger thread implementation.
    
    Polls register at configured interval and stores readings in buffer.
    """
    addr = handle._logger_addr
    reg = handle._logger_reg
    length = handle._logger_len
    interval_s = handle._logger_interval_ms / 1000.0
    
    while not handle._stop_logger_event.is_set():
        try:
            # Read from device
            with handle._lock:
                if not handle._bus:
                    break
                
                data = handle._bus.read_i2c_block_data(addr, reg, length)
                timestamp = handle._get_timestamp()
                handle._total_reads += 1
                handle._reading_counter += 1
                reading_id = handle._reading_counter
            
            # Create reading record
            reading = I2CReading(
                reading_id=reading_id,
                timestamp=timestamp,
                address=addr,
                register=reg,
                data=bytes(data),
                operation="READ"
            )
            
            # Store in buffer (serialize reading to bytes)
            # Format: [timestamp(8)][addr(1)][reg(1)][len(1)][data(N)]
            buf_data = (
                int(timestamp * 1_000_000).to_bytes(8, 'little') +
                addr.to_bytes(1, 'little') +
                reg.to_bytes(1, 'little') +
                len(data).to_bytes(1, 'little') +
                bytes(data)
            )
            cb_write(handle._buffer, buf_data)
        
        except Exception:
            with handle._lock:
                handle._total_errors += 1
        
        # Sleep for interval (check stop event periodically)
        handle._stop_logger_event.wait(interval_s)


# =============================================================================
# Status Query
# =============================================================================

def i2c_get_status(handle: I2CHandle) -> BusInfo:
    """
    Get current bus status.
    
    Args:
        handle: I2C handle
    
    Returns:
        BusInfo with current metrics
    """
    if not handle._buffer:
        return BusInfo(
            is_open=handle._is_open,
            is_logger_running=handle._logger_running,
            buffer_capacity=0,
            buffer_fill_bytes=0,
            buffer_fill_percent=0,
            buffer_overflows=0,
            total_reads=0,
            total_writes=0,
            total_errors=0
        )
    
    with handle._lock:
        total_reads = handle._total_reads
        total_writes = handle._total_writes
        total_errors = handle._total_errors
    
    return BusInfo(
        is_open=handle._is_open,
        is_logger_running=handle._logger_running,
        buffer_capacity=cb_capacity(handle._buffer),
        buffer_fill_bytes=cb_fill_bytes(handle._buffer),
        buffer_fill_percent=cb_fill_pct(handle._buffer),
        buffer_overflows=cb_overflows(handle._buffer),
        total_reads=total_reads,
        total_writes=total_writes,
        total_errors=total_errors
    )
