import re
from copy import deepcopy
from dataclasses import Field, asdict, dataclass, field, fields
from datetime import datetime
from typing import Generator, Literal

from dateutil.parser import parse

from epstein_files.util.constant.names import *
from epstein_files.util.constant.strings import *
from epstein_files.util.data import remove_zero_time, without_falsey

DuplicateType = Literal['earlier', 'quoted', 'redacted', 'same']
Metadata = dict[str, bool | datetime | int | str | list[str | None] |dict[str, bool | str]]

# Misc
CONSTANTIZE_NAMES = False  # A flag set to True that causes repr() of these classes to return strings of usable code
INDENT = '    '
INDENT_NEWLINE = f'\n{INDENT}'
INDENTED_JOIN = f',{INDENT_NEWLINE}'
MAX_LINE_LENGTH = 150
REPUTATION_MGMT = f'{REPUTATION} management'
SAME = 'same'

DUPE_TYPE_STRS: dict[DuplicateType, str] = {
    'earlier': 'an earlier draft of',
    'quoted': 'quoted in full in',
    'redacted': 'a redacted version of',
    SAME: 'the same as',
}

FIELD_SORT_KEY = {
    'id': 'a',
    'author': 'aa',
    'attribution_reason': 'zz',
}

FINANCIAL_REPORTS_AUTHORS = [
    BOFA_MERRILL,
    DEUTSCHE_BANK,
    ELECTRON_CAPITAL_PARTNERS,
    GOLDMAN_INVESTMENT_MGMT,
    'Invesco',
    JP_MORGAN,
    'Morgan Stanley',
    'S&P',
]

# Fields like timestamp and author are better added from the Document object
NON_METADATA_FIELDS = [
    'actual_text',
    'date',
    'id',
    'is_synthetic',
]


@dataclass(kw_only=True)
class DocCfg:
    """
    Encapsulates info about files that needs to be manually configured because it cannot be programmatically inferred.

    Attributes:
        id (str): ID of file
        author (Name): Author of the document (if any)
        category (str | None): Type of file
        date (str | None): If passed will be immediated parsed into the 'timestamp' field
        dupe_type (DuplicateType | None): The type of duplicate this file is or its 'duplicate_ids' are
        duplicate_ids (list[str]): IDs of *other* documents that are dupes of this document
        duplicate_of_id (str | None): If this is a dupe the ID of the duplicated file. This file will be suppressed
        is_interesting (bool | None): Override other considerations and always consider this file interesting (or not)
        timestamp (datetime | None): Time this email was sent, file was created, article published, etc.
        is_synthetic (bool): True if this config was generated by the duplicate_cfgs() method
    """
    id: str
    attached_to_email_id: str | None = None
    author: Name = None
    category: str | None = None
    date: str | None = None
    description: str | None = None
    dupe_type: DuplicateType | None = None
    duplicate_ids: list[str] = field(default_factory=list)
    duplicate_of_id: str | None = None
    is_attribution_uncertain: bool = False
    is_interesting: bool | None = None
    is_synthetic: bool = False
    timestamp: datetime | None = None

    def __post_init__(self):
        if self.date:
            self.timestamp = parse(self.date)

        if self.duplicate_of_id or self.duplicate_ids:
            self.dupe_type = self.dupe_type or SAME

    def complete_description(self) -> str | None:
        """String that summarizes what is known about this document."""
        description = ''

        if self.category and not self.description and not self.author:
            if self.category == JUNK:
                return None
            else:
                description = self.category
        elif self.category == REPUTATION:
            author_str = f"{self.author} " if self.author else ''
            description = f"{REPUTATION_MGMT}: {author_str}{self.description}"
        elif self.category == SKYPE_LOG:
            msg = f"{self.category} of conversation with {self.author}" if self.author else self.category
            description = f"{msg} {self.description}" if self.description else msg
        elif self.author and self.description:
            if self.category in [ACADEMIA, BOOK]:
                title = self.description if '"' in self.description else f'"{self.description}"'
                description = f"{title} by {self.author}"
            elif self.category == FINANCE and self.author in FINANCIAL_REPORTS_AUTHORS:
                description = f'{self.author} report: "{self.description}"'
            elif self.category == LEGAL and 'v.' in self.author:
                description = f"{self.author}: {self.description}"

        if not description:
            pieces = without_falsey([self.author, self.description])

            if pieces:
                description = ' '.join(pieces)
            else:
                return None

        if self.attached_to_email_id:
            description += f" attached to email {self.attached_to_email_id}"

        return description

    def duplicate_cfgs(self) -> Generator['DocCfg', None, None]:
        """Create synthetic DocCfg objects that set the 'duplicate_of_id' field to point back to this object."""
        for id in self.duplicate_ids:
            dupe_cfg = deepcopy(self)
            dupe_cfg.id = id
            dupe_cfg.duplicate_of_id = self.id
            dupe_cfg.duplicate_ids = []
            dupe_cfg.dupe_type = self.dupe_type
            dupe_cfg.is_synthetic = True
            yield dupe_cfg

    def metadata(self) -> Metadata:
        return {k: v for k, v in asdict(self).items() if k not in NON_METADATA_FIELDS and v}

    def _props_strs(self) -> list[str]:
        props = []
        add_prop = lambda f, value: props.append(f"{f.name}={value}")

        for _field in sorted(fields(self), key=lambda f: FIELD_SORT_KEY.get(f.name, f.name)):
            value = getattr(self, _field.name)

            if value is None or value is False or (isinstance(value, list) and len(value) == 0):
                continue
            elif _field.name == AUTHOR:
                add_prop(_field, constantize_name(str(value)) if CONSTANTIZE_NAMES else f"'{value}'")
            elif _field.name == 'category' and value in [EMAIL, TEXT_MESSAGE]:
                continue
            elif _field.name == 'recipients' and value:
                recipients_str = str([constantize_name(r) if (CONSTANTIZE_NAMES and r) else r for r in value])
                add_prop(_field, recipients_str.replace("'", '') if CONSTANTIZE_NAMES else recipients_str)
            elif _field.name == 'timestamp' and self.date is not None:
                continue  # Don't print both timestamp and date
            elif isinstance(value, datetime):
                value_str = remove_zero_time(value)
                add_prop(_field, f"parse('{value_str}')" if CONSTANTIZE_NAMES else f"'{value}'")
            elif isinstance(value, str):
                if "'" in value:
                    value = '"' + value.replace('"', r'\"') + '"'
                else:
                    value = "'" + value.replace("'", r'\'') + "'"

                add_prop(_field, value)
            else:
                add_prop(_field, str(value))

        return props

    def __repr__(self) -> str:
        props = self._props_strs()
        type_str = f"{type(self).__name__}("
        single_line_repr = type_str + ', '.join(props) + f')'

        if len(single_line_repr) < MAX_LINE_LENGTH:
            repr_str = single_line_repr
        else:
            repr_str = f"{type_str}{INDENT_NEWLINE}" + INDENTED_JOIN.join(props)
            repr_str += ',' if props else ''
            repr_str += '\n)'

        if CONSTANTIZE_NAMES:
            repr_str = INDENT + INDENT_NEWLINE.join(repr_str.split('\n'))
            return repr_str.replace(',,', ',').replace(',),', '),').replace(',),', '),')
        else:
            return repr_str


@dataclass(kw_only=True)
class CommunicationCfg(DocCfg):
    """
    Manual config is always required for MessengerLog author attribution. It's also often needed for Email
    files to handle the terrible OCR text that Congress provided which messes up a lot of the email headers.

    Attributes:
        attribution_reason (str | None): Optional explanation of why this email was attributed to this author.
        is_attribution_uncertain (bool): True if we have a good idea of who the author is but are not 100% certain
    """
    attribution_reason: str | None = None

    def __repr__(self) -> str:
        return super().__repr__()


@dataclass(kw_only=True)
class EmailCfg(CommunicationCfg):
    """
    Attributes:
        actual_text (str | None): In dire cases of broken OCR we just configure the body of the email as a string.
        fwded_text_after (str | None): If set, any text after this is a fwd of an article or similar
        is_fwded_article (bool): True if this is a newspaper article someone fwded. Used to exclude articles from word counting.
        recipients (list[Name]): Who received the email
        subject (str): Subject line
    """
    actual_text: str | None = None
    fwded_text_after: str | None = None
    is_fwded_article: bool = False
    recipients: list[Name] = field(default_factory=list)
    subject: str | None = None

    # This is necessary because for some dumb reason @dataclass(repr=False) doesn't cut it
    def __repr__(self) -> str:
        return super().__repr__()


@dataclass(kw_only=True)
class TextCfg(CommunicationCfg):
    # This is necessary because for some dumb reason @dataclass(repr=False) doesn't cut it
    def __repr__(self) -> str:
        return super().__repr__()
