import json
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
from epstein_files.util.env import args

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

# Misc
INDENT = '    '
INDENT_NEWLINE = f'\n{INDENT}'
INDENTED_JOIN = f',{INDENT_NEWLINE}'
MAX_LINE_LENGTH = 135
REPUTATION_MGMT = f'{REPUTATION} management'
SAME = 'same'

DUPE_TYPE_STRS: dict[DuplicateType, str] = {
    'bounced': 'a bounced copy of',
    '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',
    'comment': 'zz',
    'duplicate_ids': 'dup',
    'duplicate_of_id': 'dupe',
    'recipients': 'aaa',
}

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',
    '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): Parsed to a datetime by timestamp() if it exists
        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)
        is_synthetic (bool): True if this config was generated by the duplicate_cfgs() method
        replace_text_with (bool): True if `description` should replace body of the document when printing.
    """
    id: str
    attached_to_email_id: str | None = None
    author: Name = None
    category: str | None = None
    comment: str = ''
    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
    replace_text_with: str = ''

    @property
    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

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

        if self.is_interesting is False:
            metadata['is_interesting'] = False

        return metadata

    @property
    def timestamp(self) -> datetime | None:
        if self.date:
            return parse(self.date)

    def __post_init__(self):
        if self.duplicate_of_id or self.duplicate_ids:
            self.dupe_type = self.dupe_type or SAME

    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 _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 _field.name in ['actual_text', 'is_fwded_article', 'is_interesting']:  # fields can be False or None or ''
                if value is not None:
                    add_prop(_field, str(value))
            elif not value or _field.name == 'dupe_type' and value == 'same':
                continue
            elif _field.name == AUTHOR:
                add_prop(_field, constantize_name(str(value)) if args.constantize else f"'{value}'")
            elif _field.name == 'recipients':
                recipients_str = str([constantize_name(r) if (args.constantize and r) else r for r in value])
                add_prop(_field, recipients_str.replace("'", '') if args.constantize else recipients_str)
            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 or (self.comment and getattr(self, 'is_fwded_article')):
            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 args.constantize:
            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 | None = None
    recipients: list[Name] = field(default_factory=list)
    subject: str | None = None
    truncate_to: int | 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__()
