import datetime
import re
from datetime import timezone, timedelta
from pathlib import Path
from typing import Match, Optional, Union

import numpy as np

from qgis.PyQt.QtCore import QDateTime, Qt
from qgis.core import QgsEditorWidgetSetup, QgsField, QgsFields, QgsPointXY
from ..core.spectralprofile import prepareProfileValueDict, SpectralProfileFileReader
from ...qgisenums import QMETATYPE_QDATETIME, QMETATYPE_QSTRING

# GPS Longitude  DDDmm.mmmmC
# GPS Latitude  DDmm.mmmmC
# GPS Time  HHmmSS.SSS

# Precompiled regular expressions for performance
rxSIGFile = re.compile(r'\.sig$')
rxGPSLongitude = re.compile(r'(?P<deg>\d{3})(?P<min>\d{2}.\d+)(?P<quad>[EW])')
rxGPSLatitude = re.compile(r'(?P<deg>\d{2})(?P<min>\d{2}.\d+)(?P<quad>[NS])')
rxGPSTime = re.compile(r'(?P<hh>\d{2})(?P<mm>\d{2})(?P<sec>\d{2}(\.\d+)?)')
rx_decimal_sep = re.compile(r'temp= \d+([,.])\d+,')
rx_sig_file = re.compile(r'\.sig$')
rx_comment_block = re.compile(r'/[*]{3}.*[*]{3}/', re.DOTALL)
rx_metadata = re.compile(r'^(?P<tag>[^=]+)=(?P<val>.*)', re.MULTILINE)
rx_data_block = re.compile(r'^data=(?P<data>.*)', re.MULTILINE | re.DOTALL)
rx_moc_suffix = re.compile(r'_moc$')


def toQDateTime(value) -> QDateTime:
    if isinstance(value, str):
        return QDateTime.fromString(value, Qt.ISODate)
    elif isinstance(value, datetime.datetime):
        return QDateTime.fromString(value.isoformat(), Qt.ISODate)
    raise NotImplementedError()


def gpsTime(date: datetime.datetime, gpstime_string: str) -> Optional[datetime.datetime]:
    if not isinstance(date, datetime.datetime):
        return None
    m = rxGPSTime.match(gpstime_string)

    if not m:
        return None

    dtg = datetime.datetime(year=date.year,
                            month=date.month,
                            day=date.day,
                            hour=int(m.group('hh')),
                            minute=int(m.group('mm')),
                            second=int(float(m.group('sec'))),
                            tzinfo=timezone(timedelta(0)),  # UTC
                            )

    return dtg


def match_to_coordinate(matchLon: Match, matchLat: Match) -> QgsPointXY:
    y = float(matchLat.group('deg')) + float(matchLat.group('min')) / 60
    x = float(matchLon.group('deg')) + float(matchLon.group('min')) / 60

    if matchLon.group('quad') == 'W':
        x *= -1

    if matchLat.group('quad') == 'S':
        y *= -1

    return QgsPointXY(x, y)


class SVCSigFile(SpectralProfileFileReader):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.mRemoveOverlap: bool = True
        self.mPicture: Optional[Path] = None
        self.mGpsTimeR: Optional[datetime.datetime] = None
        self.mGpsTimeT: Optional[datetime.datetime] = None
        if self.mPath:
            self._readSIGFile(self.mPath)

    @classmethod
    def id(cls) -> str:
        return 'SVC'

    @classmethod
    def canReadFile(cls, path: Union[str, Path]) -> bool:
        path = Path(path)
        return rxSIGFile.search(path.name) is not None

    def _parse_coordinates(self) -> None:
        """Parse coordinate information from metadata using precompiled regex"""
        if 'longitude' in self.mMetadata and 'latitude' in self.mMetadata:
            longitudes = rxGPSLongitude.finditer(self.mMetadata['longitude'])
            latitudes = rxGPSLatitude.finditer(self.mMetadata['latitude'])
            coordinates = [match_to_coordinate(lon, lat) for lon, lat in zip(longitudes, latitudes)]

            if len(coordinates) == 2:
                self.mReferenceCoordinate = coordinates[0]
                self.mTargetCoordinate = coordinates[1]
            elif len(coordinates) == 1:
                self.mTargetCoordinate = coordinates[0]

    def _parse_times(self, fmt: Union[str] = None) -> None:
        """Parse time information from metadata efficiently"""
        if 'time' in self.mMetadata:
            # Parse regular timestamps
            t1, t2 = [t.strip() for t in self.mMetadata['time'].split(',')]
            self.mReferenceTime = self._readDateTime(t1, fmt=fmt)
            self.mTargetTime = self._readDateTime(t2, fmt=fmt)

            # Parse GPS times if available
            if 'gpstime' in self.mMetadata:
                gpsR, gpsT = [t.strip() for t in self.mMetadata['gpstime'].split(',')]

                # Only calculate GPS times if we have reference times
                if self.mReferenceTime:
                    self.mGpsTimeR = gpsTime(self.mReferenceTime, gpsR)
                if self.mTargetTime:
                    self.mGpsTimeT = gpsTime(self.mTargetTime, gpsT)

    def picturePath(self) -> Optional[Path]:
        return self.mPicture

    def standardFields(self) -> QgsFields:

        fields = super().standardFields()

        gpsTimeTField = QgsField('gpsTimeT', type=QMETATYPE_QDATETIME)
        gpsTimeRField = QgsField('gpsTimeR', type=QMETATYPE_QDATETIME)

        pictureField = QgsField(self.KEY_Picture, type=QMETATYPE_QSTRING)

        # setup attachment widget
        config = {'DocumentViewer': 1,
                  'DocumentViewerHeight': 0,
                  'DocumentViewerWidth': 0,
                  'FileWidget': True,
                  'FileWidgetButton': True,
                  'FileWidgetFilter': '',
                  'PropertyCollection': {'name': None,
                                         'properties': {},
                                         'type': 'collection'},
                  'RelativeStorage': 0,
                  'StorageAuthConfigId': None,
                  'StorageMode': 0,
                  'StorageType': None}
        setup = QgsEditorWidgetSetup('ExternalResource', config)
        pictureField.setEditorWidgetSetup(setup)
        fields.append(gpsTimeRField)
        fields.append(gpsTimeTField)
        fields.append(pictureField)
        return fields

    def asMap(self) -> dict:

        data = super().asMap()
        if isinstance(self.mPicture, Path):
            data[self.KEY_Picture] = self.mPicture.as_posix()
        if self.mGpsTimeR:
            data['gpsTimeR'] = toQDateTime(self.mGpsTimeR)
        if self.mGpsTimeT:
            data['gpsTimeT'] = toQDateTime(self.mGpsTimeT)
        return data

    @staticmethod
    def _readDateTime(text: str, fmt: Optional[str] = None) -> datetime.datetime:
        text = text.strip()

        if isinstance(fmt, str):
            return datetime.datetime.strptime(text, fmt)

        # test for ISO
        try:
            return datetime.datetime.fromisoformat(text)
        except ValueError:
            pass

        try:
            import dateutil.parser
            from dateutil.parser import ParserError
            try:
                return dateutil.parser.parse(text)
            except ParserError:
                pass
        except ImportError:
            pass

        # test non-ISO formats
        formats = [
            '%d.%m.%Y %H:%M:%S',  # 27.05.2025 09:39:32
            '%m/%d/%Y %I:%M:%S%p',  # 5/27/2025 9:39:32AM
            '%m/%d/%Y %I:%M:%S %p',  # 5/27/2025 9:39:32 AM
            '%m/%d/%Y %H:%M:%S',  # 5/27/2025 9:39:32
            '%d/%m/%Y %H:%M:%S',  # 27/05/2025 09:39:32
            '%m/%d/%Y %H:%M:%S%p',  # 5/27/2025 9:39:32AM
            '%m/%d/%Y %H:%M:%S %p',  # 5/27/2025 9:39:32 AM
        ]

        for fmt in formats:
            try:
                return datetime.datetime.strptime(text, fmt)
            except ValueError:
                pass

        raise ValueError(f'Unable to extract datetime from {text}')

    def _readSIGFile(self, path):
        """Parse SIG file using precompiled regular expressions for improved performance"""
        path: Path = Path(path)
        with open(path) as f:
            lines = f.read()

        # Remove comment blocks

        lines = rx_comment_block.sub('', lines)
        decimal_separator = rx_decimal_sep.search(lines).group(1)

        # Extract decimal separator

        # Extract metadata tags
        for match in rx_metadata.finditer(lines):
            tag = match.group('tag').strip()
            val = match.group('val').strip()
            self.mMetadata[tag] = val

        # Extract data block
        match = rx_data_block.search(lines.strip())
        if not match:
            raise ValueError(f"Could not find data block in {path}")

        data = match.group('data').strip()
        data = data.replace(decimal_separator, '.')
        dataLines = data.split('\n')
        nRows = len(dataLines)
        data = data.split()
        nCols = int(len(data) / nRows)
        data = np.asarray([float(d) for d in data]).reshape((nRows, nCols))

        # Process wavelength data
        wl = data[:, 0]
        if np.all(wl == 0):
            wl = None  # no wl defined. use band numbers only

        # Create profile value dictionaries
        self.mReference = prepareProfileValueDict(x=wl, xUnit='nm', y=data[:, 1])
        if nCols > 2:
            self.mTarget = prepareProfileValueDict(x=wl, xUnit='nm', y=data[:, 2])
        if nCols > 3:
            self.mReflectance = prepareProfileValueDict(x=wl, xUnit='nm', y=data[:, 3], yUnit='%')

        # Process coordinates if available
        self._parse_coordinates()

        # Process time information
        self._parse_times(fmt=self.dateTimeFormat())

        # Find associated image files
        stem = rx_moc_suffix.sub('', path.stem)
        for ext in ['.jpg', '.png']:
            path_img = path.parent / f'{stem}.sig{ext}'
            if path_img.is_file():
                self.mPicture = path_img
                break
