"""Pixel array extraction, ROI handling, statistics, and windowing helpers."""

from typing import Any, Dict, List

import numpy as np
import pydicom


def _get_pixel_array(ds: pydicom.Dataset) -> np.ndarray:
    """Extract pixel array with rescale slope/intercept applied.

    Handles Philips (and other vendor) rescaling so that returned values
    are in the rescaled domain (e.g. signal intensity).
    """
    pixels = ds.pixel_array.astype(np.float64)
    slope = float(getattr(ds, "RescaleSlope", 1.0) or 1.0)
    intercept = float(getattr(ds, "RescaleIntercept", 0.0) or 0.0)
    if slope != 1.0 or intercept != 0.0:
        pixels = pixels * slope + intercept
    return pixels


def _extract_roi(pixels: np.ndarray, roi: List[int]) -> np.ndarray:
    """Extract a rectangular ROI from a 2D pixel array.

    Args:
        pixels: 2D numpy array (rows, cols)
        roi: [x, y, width, height] where x,y is the top-left corner.
             Coordinates are in image space (x = column, y = row).

    Returns:
        2D numpy array of the ROI region.

    Raises:
        ValueError: If the ROI is out of bounds or invalid.
    """
    if len(roi) != 4:
        raise ValueError("ROI must be [x, y, width, height]")
    x, y, w, h = int(roi[0]), int(roi[1]), int(roi[2]), int(roi[3])
    if w <= 0 or h <= 0:
        raise ValueError(f"ROI dimensions must be positive (got {w}x{h})")
    rows, cols = pixels.shape
    if x < 0 or y < 0 or x + w > cols or y + h > rows:
        raise ValueError(
            f"ROI [{x},{y},{w},{h}] exceeds image bounds ({cols}x{rows}). "
            f"ROI right edge: {x + w}, bottom edge: {y + h}"
        )
    return pixels[y : y + h, x : x + w]


def _compute_stats(pixels: np.ndarray) -> Dict[str, Any]:
    """Compute descriptive statistics for a pixel array."""
    flat = pixels.ravel()
    return {
        "min": float(np.min(flat)),
        "max": float(np.max(flat)),
        "mean": float(np.mean(flat)),
        "std": float(np.std(flat)),
        "median": float(np.median(flat)),
        "p5": float(np.percentile(flat, 5)),
        "p25": float(np.percentile(flat, 25)),
        "p75": float(np.percentile(flat, 75)),
        "p95": float(np.percentile(flat, 95)),
        "pixel_count": int(flat.size),
    }


def _apply_windowing(
    pixels: np.ndarray, window_center: float, window_width: float
) -> np.ndarray:
    """Apply DICOM windowing to produce an 8-bit display image.

    Uses the standard DICOM linear VOI LUT:
        if pixel <= (center - width/2): output = 0
        if pixel >  (center + width/2): output = 255
        otherwise: linear mapping to 0-255
    """
    lower = window_center - window_width / 2.0
    upper = window_center + window_width / 2.0
    windowed = np.clip((pixels - lower) / (upper - lower) * 255.0, 0, 255)
    return windowed.astype(np.uint8)
