"""
This module implements a command-line interface (CLI) for running the Generalized Sequential Pattern (GSP) algorithm
on transactional data. It allows users to input transactional datasets in either JSON or CSV formats
and discover frequent sequential patterns based on a user-defined minimum support threshold.

Key Features:
1. Input Handling:
   - Supports transactional data in JSON and CSV formats.
   - Auto-detection of file type and parsing of transactions.
   - Error handling for unsupported file formats, non-existent files, and invalid data structures.

2. GSP Algorithm Integration:
   - Processes the parsed transactions using the GSP algorithm.
   - Accepts a configurable `min_support` threshold to determine the frequency of patterns.

3. User-Friendly CLI Interface:
   - `--file`: Specifies the path to the input file (JSON or CSV).
   - `--min_support`: Defines the minimum support threshold (default: 0.2).

4. Error Reporting:
   - Provides user-friendly messages for issues such as invalid input files, unsupported formats,
     and improper algorithm parameters.

5. Result Presentation:
   - Displays the discovered frequent patterns and their corresponding support counts.

This CLI empowers users to perform sequential pattern mining on transactional data efficiently through
a simple command-line interface.
"""

import os
import csv
import sys
import json
import logging
from typing import Any, Dict, List, Tuple, Union, Optional, cast

import click

from gsppy.gsp import GSP
from gsppy.utils import has_timestamps


def setup_logging(verbose: bool) -> None:
    """
    Configure logging with standardized format based on verbosity level.
    
    When verbose is enabled, provides detailed structured logging with:
    - Timestamps (ISO 8601 format)
    - Log levels
    - Process ID for traceability
    - Module context
    
    When verbose is disabled, uses simple format with just the message.
    
    Parameters:
        verbose: Whether to enable verbose logging with detailed formatting.
    """
    # Remove any existing handlers
    root_logger = logging.getLogger()
    for handler in root_logger.handlers[:]:
        root_logger.removeHandler(handler)
    
    if verbose:
        # Detailed format with timestamps, levels, PID, and context for verbose mode
        log_format = "%(asctime)s | %(levelname)-8s | PID:%(process)d | %(name)s | %(message)s"
        date_format = "%Y-%m-%dT%H:%M:%S"
        log_level = logging.DEBUG
    else:
        # Simple format for default mode - just the message
        log_format = "%(message)s"
        date_format = None
        log_level = logging.INFO
    
    # Configure logging with the appropriate format
    logging.basicConfig(
        level=log_level,
        format=log_format,
        datefmt=date_format,
        handlers=[logging.StreamHandler(sys.stdout)],
        force=True,  # Force reconfiguration even if already configured
    )


logger: logging.Logger = logging.getLogger(__name__)


def read_transactions_from_json(file_path: str) -> Union[List[List[str]], List[List[Tuple[str, float]]]]:
    """
    Read transactions from a JSON file.

    Supports both simple transactions and timestamped transactions:
    - Simple: [["A", "B", "C"], ["D", "E"]]
    - Timestamped: [[["A", 1], ["B", 3]], [["D", 2], ["E", 5]]]
      where the first element is the item and the second element is the timestamp

    Parameters:
        file_path (str): Path to the file containing transactions.

    Returns:
        Union[List[List[str]], List[List[Tuple[str, float]]]]:
            Parsed transactions from the file. For timestamped data,
            inner lists are converted to tuples (item, timestamp).

    Raises:
        ValueError: If the file cannot be read or does not contain valid JSON.
    """
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            raw_data: Any = json.load(f)

        if not isinstance(raw_data, list):
            raise ValueError("JSON must contain a top-level list of transactions.")

        raw_transactions: List[List[Union[str, Tuple[str, float]]]] = cast(
            List[List[Union[str, Tuple[str, float]]]], raw_data
        )

        # Check if this is timestamped data using the helper function.
        # Use defensive checks to avoid errors on malformed data:
        # - Find the first non-empty transaction instead of assuming index 0 is non-empty.
        # - Normalize inner list pairs (from json.load) to tuples before calling has_timestamps.
        first_non_empty_transaction: Optional[List[Union[str, Tuple[str, float]]]] = next(
            (transaction for transaction in raw_transactions if transaction),
            None,
        )

        is_timestamped = False
        if first_non_empty_transaction is not None:
            # Normalize to the exact input type expected by has_timestamps
            normalized_first: List[Union[str, Tuple[str, float]]] = []
            for item in first_non_empty_transaction:
                if isinstance(item, list) and len(item) == 2:
                    normalized_first.append((str(item[0]), float(item[1])))
                elif isinstance(item, tuple):
                    normalized_first.append(cast(Tuple[str, float], item))
                else:
                    normalized_first.append(str(item))

            is_timestamped = has_timestamps(normalized_first)

        if is_timestamped:
            # Convert timestamped data: [[["A", 1], ["B", 2]]] -> [[("A", 1), ("B", 2)]]
            transactions: List[List[Tuple[str, float]]] = [
                [cast(Tuple[str, float], tuple(item) if isinstance(item, list) else item) for item in transaction]
                for transaction in raw_transactions
            ]
            return transactions

        # Simple transactions remain as-is (or invalid data passed through for GSP to validate)
        simple_transactions: List[List[str]] = [[str(item) for item in transaction] for transaction in raw_transactions]
        return simple_transactions
    except Exception as e:
        msg = f"Error reading transaction data from JSON file '{file_path}': {e}"
        logging.error(msg)
        raise ValueError(msg) from e


def read_transactions_from_csv(file_path: str) -> List[List[str]]:
    """
    Read transactions from a CSV file.

    Parameters:
        file_path (str): Path to the file containing transactions.

    Returns:
        List[List]: Parsed transactions from the file.

    Raises:
        ValueError: If the file cannot be read or contains invalid data.
    """
    try:
        transactions: List[List[str]] = []
        with open(file_path, newline="", encoding="utf-8") as csvfile:
            reader = csv.reader(csvfile)
            for row in reader:
                # Check if the row is empty
                if not row or all(not item.strip() for item in row):
                    raise ValueError("Empty or invalid rows are not allowed in the CSV.")
                # Process valid rows
                transactions.append([item.strip() for item in row if item.strip()])
        return transactions
    except Exception as e:
        msg = f"Error reading transaction data from CSV file '{file_path}': {e}"
        logging.error(msg)
        raise ValueError(msg) from e


def detect_and_read_file(file_path: str) -> Union[List[List[str]], List[List[Tuple[str, float]]]]:
    """
    Detect file format (CSV or JSON) and read transactions.

    Parameters:
        file_path (str): Path to the file containing transactions.

    Returns:
        Union[List[List[str]], List[List[Tuple[str, float]]]]:
            Parsed transactions from the file.

    Raises:
        ValueError: If the file format is unsupported or reading fails.
    """
    if not os.path.exists(file_path):
        raise ValueError(f"File '{file_path}' does not exist.")

    _, file_extension = os.path.splitext(file_path)
    file_extension = file_extension.lower()

    if file_extension == ".json":
        return read_transactions_from_json(file_path)

    if file_extension == ".csv":
        return read_transactions_from_csv(file_path)

    raise ValueError("Unsupported file format. Please provide a JSON or CSV file.")


# Click-based CLI
@click.command()
@click.option(
    "--file",
    "file_path",
    required=True,
    type=click.Path(exists=True),
    help="Path to a JSON or CSV file containing transactions.",
)
@click.option(
    "--min_support",
    default=0.2,
    show_default=True,
    type=float,
    help="Minimum support threshold as a fraction of total transactions.",
)
@click.option(
    "--backend",
    type=click.Choice(["auto", "python", "rust", "gpu"], case_sensitive=False),
    default="auto",
    show_default=True,
    help="Backend to use for support counting.",
)
@click.option(
    "--mingap",
    type=float,
    default=None,
    help="Minimum time gap required between consecutive items in patterns (requires timestamped transactions).",
)
@click.option(
    "--maxgap",
    type=float,
    default=None,
    help="Maximum time gap allowed between consecutive items in patterns (requires timestamped transactions).",
)
@click.option(
    "--maxspan",
    type=float,
    default=None,
    help="Maximum time span from first to last item in patterns (requires timestamped transactions).",
)
@click.option("--verbose", is_flag=True, help="Enable verbose output for debugging purposes.")
def main(
    file_path: str,
    min_support: float,
    backend: str,
    mingap: Optional[float],
    maxgap: Optional[float],
    maxspan: Optional[float],
    verbose: bool,
) -> None:
    """
    Run the GSP algorithm on transactional data from a file.

    Supports both simple transactions (items only) and timestamped transactions
    (item-timestamp pairs) for temporal pattern mining.

    Examples:
        Basic usage without temporal constraints:

        ```bash
        gsppy --file transactions.json --min_support 0.3
        ```

        With temporal constraints:

        ```bash
        gsppy --file temporal_data.json --min_support 0.3 --maxgap 10
        gsppy --file events.json --min_support 0.5 --mingap 2 --maxgap 10 --maxspan 20
        ```
    """
    setup_logging(verbose)

    # Automatically detect and load transactions
    try:
        transactions = detect_and_read_file(file_path)
    except ValueError as e:
        logger.error(f"Error: {e}")
        sys.exit(1)

    # Validate parameters
    _validate_parameters(min_support, mingap, maxgap, maxspan)

    # Select backend for acceleration layer
    if backend and backend.lower() != "auto":
        os.environ["GSPPY_BACKEND"] = backend.lower()

    # Initialize and run GSP algorithm
    try:
        gsp = GSP(transactions, mingap=mingap, maxgap=maxgap, maxspan=maxspan, verbose=verbose)
        patterns: List[Dict[Tuple[str, ...], int]] = gsp.search(min_support=min_support)
        logger.info("Frequent Patterns Found:")
        for i, level in enumerate(patterns, start=1):
            logger.info(f"\n{i}-Sequence Patterns:")
            for pattern, support in level.items():
                logger.info(f"Pattern: {pattern}, Support: {support}")
    except Exception as e:
        logger.error(f"Error executing GSP algorithm: {e}")
        sys.exit(1)


def _validate_parameters(
    min_support: float,
    mingap: Optional[float],
    maxgap: Optional[float],
    maxspan: Optional[float],
) -> None:
    """
    Validate input parameters for GSP algorithm.

    Args:
        min_support: Minimum support threshold
        mingap: Minimum time gap constraint
        maxgap: Maximum time gap constraint
        maxspan: Maximum time span constraint

    Raises:
        SystemExit: If validation fails
    """
    # Check min_support
    if min_support <= 0.0 or min_support > 1.0:
        logger.error("Error: min_support must be in the range (0.0, 1.0].")
        sys.exit(1)

    # Validate temporal constraints
    if mingap is not None and mingap < 0:
        logger.error("Error: mingap must be non-negative.")
        sys.exit(1)
    if maxgap is not None and maxgap < 0:
        logger.error("Error: maxgap must be non-negative.")
        sys.exit(1)
    if maxspan is not None and maxspan < 0:
        logger.error("Error: maxspan must be non-negative.")
        sys.exit(1)
    if mingap is not None and maxgap is not None and mingap > maxgap:
        logger.error("Error: mingap cannot be greater than maxgap.")
        sys.exit(1)


if __name__ == "__main__":
    main()
