"""
File Purpose: logging, recording info about state of code, etc.

For reproducibility in the future.
E.g., with results, also save git hash for current version of the code.
"""

import datetime
import os
import subprocess
import sys

from .sentinels import UNSET


### --------------------- git_hash --------------------- ###

def git_hash_local(*, default=UNSET):
    '''returns the git commit hash for current git HEAD within the local directory

    default: any object
        if not UNSET, and can't get hash (but filepath does exist),
        return default & print message, instead of crashing if can't get hash.
    '''
    try:
        return subprocess.check_output(
            ['git', 'rev-parse', '--short', 'HEAD'],
            stderr=subprocess.DEVNULL,   # suppress crash message (crash gets handled below)
        ).decode('ascii').strip()
    except subprocess.CalledProcessError:
        if default is not UNSET:
            print(f'(failed to get git hash; returning default. At dir={repr(os.getcwd())})')
            return default
        raise

def git_hash(module_or_path='.', *, default=UNSET):
    '''returns the git commit hash for current git HEAD at the provided module or path.
    if string, treat it as a path; if module object, use path to module.__file__.
    module_or_path: string or module object, default '.'
        place to get git hash from.
        string --> path=module_or_path. If not a directory, also try os.path.dirname(path)
        module --> module=module_or_path. path=os.path.dirname(module.__file__)
    default: any object
        if provided, and can't get hash, return default & print message.
    '''
    try:
        if isinstance(module_or_path, str):
            path = module_or_path
        else:
            module = module_or_path
            path = module.__file__
        if not os.path.isdir(path):
            path = os.path.abspath(path)
            dirpath = os.path.dirname(path)
            if not os.path.isdir(dirpath):
                raise FileNotFoundError(f'{repr(path)} (not an existing directory). (\nalso {repr(dirpath)})')
            path = dirpath
        try:
            cwd0 = os.getcwd()
            os.chdir(path)
            return git_hash_local(default=default)
        finally:
            os.chdir(cwd0)
    except Exception:
        if default is not UNSET:
            print(f'(failed to get git hash; returning default. For input={repr(module_or_path)})')
            return default
        raise

def git_hash_here(globals_):
    '''returns git commit hash for the __file__ in the namespace where this function is called.'''
    return git_hash(globals_['__file__'])

def git_hash_PlasmaCalcs():
    '''returns the git commit hash for current git HEAD in PlasmaCalcs'''
    return git_hash_here(globals())


### --------------------- PlasmaCalcs version --------------------- ###

def _PlasmaCalcs_version():
    '''returns the current version of PlasmaCalcs, if available. Else, return None.'''
    try:
        from .. import __version__
    except ImportError:
        return None
    else:
        return __version__


### --------------------- datetime --------------------- ###

def datetime_astimezone(datetime_, tz='local'):
    '''convert a datetime.datetime to the requested timezone.
    Similar to datetime_.astimezone(tz), but also supports tz='local' and 'utc'.

    datetime_: datetime.datetime object
        datetime to convert.
    tz: 'local', 'utc', or datetime.tzinfo object
        timezone to convert result to.
        None --> do not convert.
        'local' --> machine's local timezone.
        'utc' --> standard/universal time (UTC) ("+00:00" if converted to str)
    '''
    if tz is None:
        pass
    elif tz == 'local':
        datetime_ = datetime_.astimezone()
    else:
        if tz == 'utc': tz = datetime.timezone.utc
        datetime_ = datetime_.astimezone(tz)
    return datetime_

def datetime_str(datetime_=None, *, sep="_", tz='local', timespec='seconds'):
    '''datetime as string, including timezone.
    Uses "isoformat", with "_" separator: YYYY-MM-DD_HH:MM:SS+HH:MM
    Datetime (timezone-aware) can be recovered via `datetime.datetime.fromisoformat(str)`

    datetime_: None or datetime.datetime object
        datetime to convert to string. If None, use datetime.datetime.now().
    sep: string
        separator between date and time. '_' nice when spaces not desireable (e.g. file names).
        'T' is nice for more universal recognizability; ' ' too but slightly less universal.
    tz: None, 'local', 'utc', or datetime.tzinfo object
        timezone to convert result to.
        None --> do not convert.
        'local' --> machine's local timezone.
        'utc' --> standard/universal time (UTC) (i.e. result will end in +00:00)
    timespec: str, probably 'seconds' or 'microseconds'
        passed to datetime.isoformat(). 'microseconds' --> YYYY-MM-DD_HH:MM:SS.ssssss+HH:MM
    '''
    if datetime_ is None:
        datetime_ = datetime.datetime.now()
    datetime_ = datetime_astimezone(datetime_, tz=tz)
    return datetime_.isoformat(sep=sep, timespec=timespec)

def datetime_now(*, sep="_", tz='local', timespec='microseconds'):
    '''current datetime as string according to user's computer. Include timezone.
    Uses "isoformat", with "_" separator: YYYY-MM-DD_HH:MM:SS.ssssss+HH:MM
    Datetime (timezone-aware) can be recovered via `datetime.datetime.fromisoformat(str)`

    sep: string
        separator between date and time. '_' nice when spaces not desireable (e.g. file names).
        'T' is nice for more universal recognizability; ' ' too but slightly less universal.
    tz: 'local', 'utc', or datetime.tzinfo object
        timezone to convert result to. 'local' --> machine's local timezone.
        'utc' --> standard/universal time (UTC) (i.e. result will end in +00:00)
    timespec: str, probably 'seconds' or 'microseconds'
        passed to datetime.isoformat(). 'microseconds' --> YYYY-MM-DD_HH:MM:SS.ssssss+HH:MM
    '''
    return datetime_str(datetime_=None, sep=sep, tz=tz, timespec=timespec)


### --------------------- file creation time (best effort) --------------------- ###

def datetimestamp(filepath, tz='local', *, as_str=False, sep="_"):
    '''return (best-effort guess of) datetime.datetime when this file was created or first written.
    Result depends on system:
        - macOS: real birth time
        - Windows: real creation time
        - Linux: falls back to modification time (st_mtime)

    filepath : str
        path to file
    tz: 'local', 'utc', or datetime.tzinfo object
        timezone to convert result to. 'local' --> machine's local timezone.
        'utc' --> standard/universal time (UTC) ("+00:00" if converted to str)
    as_str : bool
        whether to instead return as isoformat string using sep: YYYY-MM-DD_HH:MM:SS+HH:MM
    sep: string
        separator between date and time if as_str=True. (default '_' shown in format above^)
    '''
    stat = os.stat(filepath)

    # Platform-specific best-effort creation time
    if sys.platform == "win32":
        ts = stat.st_ctime  # creation time on Windows
    elif hasattr(stat, "st_birthtime"):
        ts = stat.st_birthtime  # macOS / BSD birth time
    else:
        # Linux fallback: use modification time
        ts = stat.st_mtime

    dt = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
    dt = datetime_astimezone(dt, tz=tz)

    if as_str:
        return datetime_str(dt, sep=sep, tz=None)  # tz=None because already converted above.
    else:
        return dt


### --------------------- PlasmaCalcs version --------------------- ###

def code_snapshot_info(*, default=None, skip_missing=False):
    '''returns dict including info about right now, and the version of the code being used.

    Includes as many of the following as possible (uses `default` if info unavailable due to crash):
        'pc__version__': PlasmaCalcs version
        'pc__commit_hash': git hash for PlasmaCalcs
        'datetime': current datetime, in isoformat

    default: any object, default None
        default value to use if info is unavailable due to crash.
    skip_missing: bool
        whether to skip missing info (i.e. any values which are None, probably due to crash).
    '''
    result = {}
    try:
        pc_version = _PlasmaCalcs_version()
    except Exception:
        pc_version = default
    if (not skip_missing) or (pc_version is not default):
        result['pc__version__'] = pc_version
    try:
        pc_hash = git_hash_PlasmaCalcs()
    except Exception:
        pc_hash = default
    if (not skip_missing) or (pc_hash is not default):
        result['pc__commit_hash'] = pc_hash
    try:
        datetime = datetime_now()
    except Exception:
        datetime = default
    if (not skip_missing) or (datetime is not default):
        result['datetime'] = datetime
    return result
