#! /usr/bin/env python3
#
# Decode QFiles exposing header, config, and data records
#
# Feb-2025, Pat Welch, pat@mousebrains.com

import os.path
import logging
import sys
from typing import Generator, Any, BinaryIO
from .QHeader import QHeader
from .QData import QData, QRecord


class QFile:
    """
    Context manager for reading Rockland Scientific Q-files.

    Provides convenient access to header and data records from Q-files
    generated by Rockland Scientific's ISDP data logger.

    Usage:
        with QFile("data.q") as qf:
            hdr = qf.header()
            for record in qf.data():
                process(record)
    """

    # Buffer size for sequential binary reads (64KB)
    BUFFER_SIZE = 64 * 1024

    def __init__(self, fn: str) -> None:
        """
        Initialize QFile reader.

        Args:
            fn: Path to Q-file (can include ~ for home directory)

        Raises:
            FileNotFoundError: If the file does not exist
        """
        self.__fn = os.path.abspath(os.path.expanduser(fn))
        self.__fp: BinaryIO | None = None
        self.__data: QData | None = None

        if not os.path.isfile(self.__fn):
            raise FileNotFoundError(f"{self.__fn} does not exist")

    def __enter__(self) -> "QFile":
        self.__maybeOpen__()
        return self

    def __exit__(self, *args: Any) -> None:
        if self.__fp:
            if not self.__fp.closed:
                self.__fp.close()
            self.__fp = None

    def __del__(self) -> None:
        self.__exit__()

    def __maybeOpen__(self) -> Any:
        if not self.__fp or self.__fp.closed:
            # Use optimized buffer for better sequential read performance
            self.__fp = open(self.__fn, "rb", buffering=self.BUFFER_SIZE)
        return self.__fp

    def header(self) -> QHeader:
        """
        Read and return the header record from the Q-file.

        Must be called before reading any data records.

        Returns:
            QHeader object containing file metadata, channel/spectra identifiers,
            frequencies, and configuration
        """
        fp = self.__maybeOpen__()
        fp.seek(0)  # Rewind to beginning
        hdr = QHeader(fp, self.__fn)
        self.__data = QData(hdr)
        return hdr

    def data(self) -> Generator[QRecord, None, None]:
        """
        Generator yielding data records from the Q-file.

        Handles files with multiple header/data segments by detecting
        embedded header records and re-initializing the parser.

        Yields:
            QRecord objects containing time, channels, and spectra data

        Raises:
            RuntimeError: If header() has not been called yet
        """
        if not self.__data:
            raise RuntimeError(
                f"A header must be read before any data records in {self.__fn}"
            )

        fp = self.__fp
        if fp is None:
            raise RuntimeError("File pointer is not open")
        while True:
            if QHeader.chkIdent(fp):
                hdr = QHeader(fp, self.__fn)
                self.__data = QData(hdr)
            elif QData.chkIdent(fp):
                record = self.__data.load(fp)
                if record is None:
                    break
                yield record
            else:
                break

    def prettyRecord(self, record: QRecord) -> str | None:
        """
        Format a QRecord as a human-readable string.

        Args:
            record: QRecord to format

        Returns:
            Multi-line string representation, or None if no data loaded yet
        """
        return self.__data.prettyRecord(record) if self.__data else None

    def validate(self) -> dict[str, Any]:
        """
        Validate Q-file integrity and return diagnostic information.

        Returns:
            Dictionary with validation results containing:
            - valid: Overall validity (bool)
            - version: Q-file version or None
            - records_readable: Successfully read records (int)
            - unknown_identifiers: Unrecognized sensor IDs (set)
            - errors: Error messages (list)
        """
        from .QHexCodes import QHexCodes

        results: dict[str, Any] = {
            "valid": True,
            "version": None,
            "records_readable": 0,
            "unknown_identifiers": set(),
            "errors": [],
        }

        hexmap = QHexCodes()

        try:
            # Validate header
            hdr = self.header()
            results["version"] = hdr.version

            # Check for unknown channel identifiers
            if hdr.channels:
                for ident in hdr.channels:
                    if hexmap.name(ident) is None:
                        results["unknown_identifiers"].add(ident)

            # Check for unknown spectra identifiers
            if hdr.spectra:
                for ident in hdr.spectra:
                    if hexmap.name(ident) is None:
                        results["unknown_identifiers"].add(ident)

            # Validate data records
            for record in self.data():
                results["records_readable"] += 1

        except EOFError as e:
            results["valid"] = False
            results["errors"].append(f"EOF error: {e}")
        except ValueError as e:
            results["valid"] = False
            results["errors"].append(f"Value error: {e}")
        except Exception as e:
            results["valid"] = False
            results["errors"].append(f"Unexpected error: {e}")

        return results


def main() -> None:
    """Command-line interface for QFile."""
    from argparse import ArgumentParser
    from . import __version__

    parser = ArgumentParser()
    parser.add_argument(
        "--version", action="version", version="%(prog)s " + __version__
    )
    parser.add_argument("filename", type=str, nargs="+", help="Input filename(s)")
    parser.add_argument(
        "--validate", action="store_true", help="Validate Q-file integrity"
    )
    parser.add_argument(
        "--n", type=int, default=10, help="Number of data records to print out"
    )
    parser.add_argument(
        "--logLevel",
        type=str,
        default="INFO",
        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
        help="Logging level",
    )
    args = parser.parse_args()

    logging.basicConfig(level=getattr(logging, args.logLevel))

    for fn in args.filename:
        try:
            with QFile(fn) as qf:
                if args.validate:
                    # Validation mode
                    print(f"Validating {fn}...")
                    results = qf.validate()

                    if results["valid"]:
                        print("Q-file is valid")
                    else:
                        print("Q-file has issues")

                    print(f"  Version: {results['version']}")
                    print(f"  Records readable: {results['records_readable']}")

                    if results["unknown_identifiers"]:
                        print(
                            f"  Unknown identifiers: {len(results['unknown_identifiers'])}"
                        )
                        for ident in sorted(results["unknown_identifiers"]):
                            print(f"    {ident:#06x}")

                    if results["errors"]:
                        print("  Errors:")
                        for error in results["errors"]:
                            print(f"    - {error}")

                    print()
                else:
                    # Normal display mode
                    logging.info(f"fn {fn}")
                    logging.info(f"QFile {qf}")
                    hdr = qf.header()
                    logging.info(f"hdr {hdr}")
                    for cnt, record in enumerate(qf.data()):
                        if cnt >= args.n:
                            break
                        logging.info(f"Record {qf.prettyRecord(record)}")
        except EOFError:
            logging.info(f"EOF while reading {fn}")
        except Exception:
            logging.exception(f"While reading {fn}")
            sys.exit(1)


if __name__ == "__main__":
    main()
