# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
``fitscheck`` is a command line script based on astropy.io.fits for verifying
and updating the CHECKSUM and DATASUM keywords of .fits files.  ``fitscheck``
can also detect and often fix other FITS standards violations.  ``fitscheck``
facilitates re-writing the non-standard checksums originally generated by
astropy.io.fits with standard checksums which will interoperate with CFITSIO.

``fitscheck`` will refuse to write new checksums if the checksum keywords are
missing or their values are bad.  Use ``--force`` to write new checksums
regardless of whether or not they currently exist or pass.  Use
``--ignore-missing`` to tolerate missing checksum keywords without comment.

Example uses of fitscheck:

1. Add checksums::

    $ fitscheck --write *.fits

2. Write new checksums, even if existing checksums are bad or missing::

    $ fitscheck --write --force *.fits

3. Verify standard checksums and FITS compliance without changing the files::

    $ fitscheck --compliance *.fits

4. Only check and fix compliance problems,  ignoring checksums::

    $ fitscheck --checksum none --compliance --write *.fits

5. Verify standard interoperable checksums::

    $ fitscheck *.fits

6. Delete checksum keywords::

    $ fitscheck --checksum remove --write *.fits

"""

import argparse
import logging
import sys
import warnings

from astropy import __version__
from astropy.io import fits

log = logging.getLogger("fitscheck")

DESCRIPTION = """
e.g. fitscheck example.fits

Verifies and optionally re-writes the CHECKSUM and DATASUM keywords
for a .fits file.
Optionally detects and fixes FITS standard compliance problems.

This script is part of the Astropy package. See
https://docs.astropy.org/en/latest/io/fits/usage/scripts.html#module-astropy.io.fits.scripts.fitscheck
for further documentation.
""".strip()


def handle_options(args):
    if not len(args):
        args = ["-h"]

    parser = argparse.ArgumentParser(
        description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    # TODO: pass suggest_on_error as kwarg when PYTHON_LT_14 is dropped
    parser.suggest_on_error = True

    parser.add_argument(
        "--version", action="version", version=f"%(prog)s {__version__}"
    )

    parser.add_argument(
        "fits_files", metavar="file", nargs="+", help=".fits files to process."
    )

    parser.add_argument(
        "-k",
        "--checksum",
        dest="checksum_kind",
        choices=["standard", "remove", "none"],
        help="Choose FITS checksum mode or none.  Defaults standard.",
        default="standard",
    )

    parser.add_argument(
        "-w",
        "--write",
        dest="write_file",
        help="Write out file checksums and/or FITS compliance fixes.",
        default=False,
        action="store_true",
    )

    parser.add_argument(
        "-f",
        "--force",
        dest="force",
        help="Do file update even if original checksum was bad.",
        default=False,
        action="store_true",
    )

    parser.add_argument(
        "-c",
        "--compliance",
        dest="compliance",
        help="Do FITS compliance checking; fix if possible.",
        default=False,
        action="store_true",
    )

    parser.add_argument(
        "-i",
        "--ignore-missing",
        dest="ignore_missing",
        help="Ignore missing checksums.",
        default=False,
        action="store_true",
    )

    parser.add_argument(
        "-v",
        "--verbose",
        dest="verbose",
        help="Generate extra output.",
        default=False,
        action="store_true",
    )

    global OPTIONS
    OPTIONS = parser.parse_args(args)

    if OPTIONS.checksum_kind == "none":
        OPTIONS.checksum_kind = False
    elif OPTIONS.checksum_kind == "standard":
        OPTIONS.checksum_kind = True
    elif OPTIONS.checksum_kind == "remove":
        OPTIONS.write_file = True
        OPTIONS.force = True

    return OPTIONS.fits_files


def setup_logging():
    log.handlers.clear()

    if OPTIONS.verbose:
        log.setLevel(logging.INFO)
    else:
        log.setLevel(logging.WARNING)

    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(message)s"))
    log.addHandler(handler)


def verify_checksums(filename):
    """
    Prints a message if any HDU in `filename` has a bad checksum or datasum.
    """
    checksum_errors = 0

    # _verify_checksum_datasum issues warnings when checksums are wrong so we
    # catch those to avoid duplicates, since we are using logging here to
    # report problems.
    with warnings.catch_warnings(record=True):
        warnings.simplefilter("always")
        with fits.open(filename, checksum=OPTIONS.checksum_kind) as hdulist:
            for i, hdu in enumerate(hdulist):
                # looping on HDUs is needed to read them and verify the
                # checksums
                if not OPTIONS.ignore_missing and not hdu._checksum:
                    checksum_errors += 1
                    log.warning(
                        f"MISSING {filename!r} .. Checksum not found in HDU #{i}",
                    )
                elif hdu._checksum and not hdu._checksum_valid:
                    checksum_errors += 1
                    log.warning(
                        f"BAD {filename!r} Checksum verification failed for HDU {i}"
                    )

                if not OPTIONS.ignore_missing and not hdu._datasum:
                    checksum_errors += 1
                    log.warning(
                        f"MISSING {filename!r} .. Datasum not found in HDU #{i}",
                    )
                elif hdu._datasum and not hdu._datasum_valid:
                    checksum_errors += 1
                    log.warning(
                        f"BAD {filename!r} Datasum verification failed for HDU {i}"
                    )

    if not checksum_errors:
        log.info(f"OK {filename!r}")
    return checksum_errors


def verify_compliance(filename):
    """Check for FITS standard compliance."""
    with fits.open(filename) as hdulist:
        try:
            hdulist.verify("exception")
        except fits.VerifyError as exc:
            log.warning("NONCOMPLIANT %r .. %s", filename, str(exc).replace("\n", " "))
            return 1
    return 0


def update(filename):
    """
    Sets the ``CHECKSUM`` and ``DATASUM`` keywords for each HDU of `filename`.

    Also updates fixes standards violations if possible and requested.
    """
    output_verify = "silentfix" if OPTIONS.compliance else "ignore"

    # For unit tests we reset temporarily the warning filters. Indeed, before
    # updating the checksums, fits.open will verify the existing checksums and
    # raise warnings, which are later caught and converted to log.warning...
    # which is an issue when testing, using the "error" action to convert
    # warnings to exceptions.
    with warnings.catch_warnings():
        warnings.resetwarnings()
        with fits.open(
            filename,
            do_not_scale_image_data=True,
            checksum=OPTIONS.checksum_kind,
            mode="update",
        ) as hdulist:
            hdulist.flush(output_verify=output_verify)


def process_file(filename):
    """
    Handle a single .fits file,  returning the count of checksum and compliance
    errors.
    """
    try:
        checksum_errors = verify_checksums(filename)
        if OPTIONS.compliance:
            compliance_errors = verify_compliance(filename)
        else:
            compliance_errors = 0
        if (OPTIONS.write_file and checksum_errors == 0) or OPTIONS.force:
            update(filename)
        return checksum_errors + compliance_errors
    except Exception as e:
        log.error(f"EXCEPTION {filename!r} .. {e}")
        return 1


def main(args=None):
    """
    Processes command line parameters into options and files,  then checks
    or update FITS DATASUM and CHECKSUM keywords for the specified files.
    """
    errors = 0
    fits_files = handle_options(args or sys.argv[1:])
    setup_logging()
    for filename in fits_files:
        errors += process_file(filename)
    if errors:
        log.warning(f"{errors} errors")
    return int(bool(errors))
