"""DICOM Sorting Utilities.

This module provides utilities for sanitizing filenames, truncating UIDs,
and reading specific tags from DICOM files.

Functions
---------
sanitize_file_name(filename: str) -> str
    Sanitize filenames by replacing potentially dangerous characters.
truncate_uid(uid: str, last_digits: int = 5) -> str
    Truncate a UID to the last n characters.
read_tags(file: Path, tags: list[str], truncate: bool = True, sanitize: bool = True) -> dict[str, str]
    Read specified tags from a DICOM file.

Examples
--------
Sanitize a filename:
    >>> sanitize_file_name('test<>file:/name.dcm')
    'test_file_name.dcm'

Truncate a UID:
    >>> truncate_uid('1.2.840.10008.1.2.1', last_digits=5)
    '.1.2.1'

Read tags from a DICOM file:
    >>> from pathlib import Path
    >>> tags = ['PatientID', 'StudyInstanceUID']
    >>> read_tags(Path('sample.dcm'), tags)
    {'PatientID': '12345', 'StudyInstanceUID': '1.2.3.4.5'}
"""

import re
from pathlib import Path
from typing import Dict, List

from pydicom import dcmread
from pydicom.errors import InvalidDicomError

from imgtools.logging import logger


def sanitize_file_name(filename: str) -> str:
	"""
	Sanitize the file name by replacing potentially dangerous characters.

	Replaces disallowed characters with underscores, converts spaces to
	underscores, and removes subsequent underscores.

	Parameters
	----------
	filename : str
	    The input file name to sanitize.

	Returns
	-------
	str
	    The sanitized file name.

	Examples
	--------
	>>> sanitize_file_name('test<>file:/name.dcm')
	'test_file_name.dcm'
	>>> sanitize_file_name('my file name.dcm')
	'my_file_name.dcm'
	"""
	assert filename is not None
	assert isinstance(filename, str)

	disallowed_characters_pattern = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
	sanitized_name = disallowed_characters_pattern.sub('_', filename)
	sanitized_name = sanitized_name.replace(' ', '_')
	sanitized_name = re.sub(r'(_{2,})', '_', sanitized_name)
	return sanitized_name


def truncate_uid(uid: str, last_digits: int = 5) -> str:
	"""
	Truncate the UID to the last n characters (including periods and underscores).

	If the UID is shorter than `last_digits`, the entire UID is returned.

	Parameters
	----------
	uid : str
	    The UID string to truncate.
	last_digits : int, optional
	    The number of characters to keep at the end of the UID (default is 5).

	Returns
	-------
	str
	    The truncated UID string.

	Examples
	--------
	>>> truncate_uid('1.2.840.10008.1.2.1', last_digits=5)
	'.1.2.1'
	>>> truncate_uid('12345', last_digits=10)
	'12345'
	"""
	assert uid is not None
	assert isinstance(uid, str)
	assert isinstance(last_digits, int)
	if last_digits >= len(uid) or last_digits <= 0:
		return uid

	return uid[-last_digits:]


def read_tags(
	file: Path,
	tags: List[str],
	truncate: bool = True,
	sanitize: bool = True,
) -> Dict[str, str]:
	"""
	Read the specified tags from a DICOM file.

	Reads a set of tags from a DICOM file and applies optional sanitization
	and truncation for UIDs. Handles cases where specific tags may be missing.

	Parameters
	----------
	file : Path
	                                Path to the DICOM file.
	tags : list of str
	                                List of DICOM tags to read.
	truncate : bool, optional
	                                If True, truncate UIDs to the last 5 characters (default is True).
	sanitize : bool, optional
	                                If True, sanitize tag values to remove disallowed characters
	                                (default is True).

	Returns
	-------
	dict of str : str
	                                A dictionary mapping tags to their values.

	Raises
	------
	TypeError
	                                If there is a type error while reading the DICOM file.
	InvalidDicomError
	                                If the file is not a valid DICOM file.
	ValueError
	                                If there is a value error while reading the file.

	Notes
	-----
	For RTSTRUCT files, missing 'InstanceNumber' tags default to '1'.

	Examples
	--------
	Read tags from a valid DICOM file with truncation:
	>>> from pathlib import Path
	>>> read_tags(Path('sample.dcm'), ['PatientID', 'StudyInstanceUID'])
	{'PatientID': '12345', 'StudyInstanceUID': '1.2.3.4.5'}

	Read tags without truncating UIDs:
	>>> read_tags(Path('sample.dcm'), ['PatientID', 'StudyInstanceUID'], truncate=False)
	{'PatientID': '12345', 'StudyInstanceUID': '1.2.840.10008.1.2.1'}

	Handle missing tags:
	>>> read_tags(Path('sample.dcm'), ['NonexistentTag'])
	[warn] No value for tag: NonexistentTag in file: sample.dcm
	{'NonexistentTag': 'UNKNOWN'}
	"""
	assert isinstance(file, Path)
	assert isinstance(tags, list) and all(isinstance(tag, str) for tag in tags) and tags is not None

	try:
		dicom = dcmread(file, specific_tags=tags, stop_before_pixels=True)
	except FileNotFoundError as fnfe:
		errmsg = f'File not found: {file}'
		raise FileNotFoundError(errmsg) from fnfe
	except InvalidDicomError as ide:
		errmsg = f'Invalid DICOM file: {file}'
		raise InvalidDicomError(errmsg) from ide
	except ValueError as ve:
		errmsg = f'Value error reading DICOM file: {file}'
		raise ValueError(errmsg) from ve

	result = {}
	is_rtstruct = dicom.get('Modality') == 'RTSTRUCT' if 'InstanceNumber' in tags else False
	# TODO: this logic isnt robust...

	for tag in tags:
		value = (
			truncate_uid(str(dicom.get(tag, '')))
			if tag.endswith('UID') and truncate
			else str(dicom.get(tag, 'UNKNOWN'))
		)
		if value == 'UNKNOWN':
			if tag in ['InstanceNumber', 'AccessionNumber'] and is_rtstruct:
				value = '1'
			else:
				logger.warning(f'No value for tag: `{tag}` in file: {file}')
		elif value == '':
			logger.warning(f'Empty value for tag: `{tag}` in file: {file}')
			value = 'UNKNOWN'
		result[tag] = sanitize_file_name(value) if sanitize else value

	return result
