#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0

"""ocrmypdf command line entrypoint."""

from __future__ import annotations

import logging
import multiprocessing
import os
import signal
import sys
from collections.abc import Mapping
from contextlib import suppress
from typing import Any

from pydantic import ValidationError

from ocrmypdf import __version__
from ocrmypdf._pipelines.ocr import run_pipeline_cli
from ocrmypdf._validation import check_options
from ocrmypdf.api import Verbosity, configure_logging, configure_stdout_protection
from ocrmypdf.cli import get_options_and_plugins
from ocrmypdf.exceptions import (
    BadArgsError,
    ExitCode,
    InputFileError,
    MissingDependencyError,
)

log = logging.getLogger('ocrmypdf')


def sigbus(*args):
    """Handle SIGBUS signals.

    pikepdf, depending on configuration, may use mmap so SIGBUS is a
    possibility.
    """
    raise InputFileError("Lost access to the input file")


def _describe_options_error(exc: ValueError) -> str:
    """Reduce an options validation error to the part a CLI user needs to read."""
    if isinstance(exc, ValidationError):
        return '\n'.join(_describe_single_error(error) for error in exc.errors())
    return str(exc)


def _describe_single_error(error: Mapping[str, Any]) -> str:
    """Render one pydantic error as a line naming the option that was rejected.

    Messages raised by our own validators already name the option they are about,
    so they are passed through. Messages generated by pydantic's own type and
    constraint checks ("Input should be...") do not, so the offending field is
    prefixed as the command line option that sets it.
    """
    msg = str(error['msg'])
    if error.get('type') == 'value_error':
        return msg.removeprefix('Value error, ')
    loc = error.get('loc') or ()
    if not loc:
        return msg
    option = '--' + str(loc[0]).replace('_', '-')
    return f"{option}: {msg}"


def run(args=None):
    """Run the ocrmypdf command line interface."""
    # Protect the real stdout before loading plugins or starting any worker
    # processes/threads, so that only our final PDF output can reach it and
    # stray writes from plugins or libraries are diverted to stderr.
    configure_stdout_protection()

    # Logging is not configured yet, since its settings come from the options we
    # are about to parse. logging.lastResort prints the bare message to stderr,
    # which matches the format configure_logging() would install anyway.
    try:
        options, plugin_manager = get_options_and_plugins(args=args)
    except BadArgsError as e:
        log.error(e)
        return e.exit_code
    except ValueError as e:
        log.error(_describe_options_error(e))
        return ExitCode.bad_args

    with suppress(AttributeError, PermissionError):
        os.nice(5)

    verbosity = Verbosity(options.verbose)
    if not os.isatty(sys.stderr.fileno()):
        options.progress_bar = False
    if options.quiet:
        verbosity = Verbosity.quiet
        options.progress_bar = False
    configure_logging(
        verbosity,
        progress_bar_friendly=options.progress_bar,
        manage_root_logger=True,
        plugin_manager=plugin_manager,
    )
    log.debug('ocrmypdf %s', __version__)
    try:
        check_options(options, plugin_manager)
    except ValueError as e:
        log.error(e)
        return ExitCode.bad_args
    except BadArgsError as e:
        log.error(e)
        return e.exit_code
    except MissingDependencyError as e:
        log.error(e)
        return ExitCode.missing_dependency

    with suppress(AttributeError, OSError):
        signal.signal(signal.SIGBUS, sigbus)

    result = run_pipeline_cli(options=options, plugin_manager=plugin_manager)
    return result


if __name__ == '__main__':
    multiprocessing.freeze_support()
    if sys.platform not in ('win32', 'darwin'):
        with suppress(RuntimeError):
            multiprocessing.set_start_method('forkserver')
    sys.exit(run())
