import typing
from datetime import datetime
from os.path import basename
from posixpath import join as urljoin

from requests import HTTPError

from reptor.api.APIClient import APIClient
from reptor.models.Note import Note
from reptor.utils.file_operations import guess_filetype

from reptor.models.Note import NoteTemplate


class NotesAPI(APIClient):
    """Interacts with Notes Endpoints

    Args:
        APIClient (_type_): _description_
    """

    def __init__(self, **kwargs) -> None:
        super().__init__(require_project_id=False, **kwargs)

        if self.private_note:
            self.base_endpoint = urljoin(
                self.reptor.get_config().get_server(),
                "api/v1/pentestusers/self/notes/",
            )
        elif self.project_id:
            self.base_endpoint = urljoin(
                self.reptor.get_config().get_server(),
                f"api/v1/pentestprojects/{self.project_id}/notes/",
            )

    @property
    def private_note(self):
        return self.reptor.get_config().get("private_note")

    def get_notes(self) -> typing.List[Note]:
        """
        Gets list of notes
        (Notes are not paginated)
        """
        self.debug("Getting Notes List")
        response = self.get(self.base_endpoint)
        notes = list()
        for note_data in response.json():
            notes.append(Note(note_data))
        return notes

    def get_note(
        self,
        noteid: typing.Optional[str] = None,
        notetitle: typing.Optional[str] = None,
    ) -> typing.Optional[Note]:
        for note in self.get_notes():
            if noteid:
                if note.id == noteid:
                    return note
            elif notetitle:
                if note.title == notetitle:
                    return note
            else:
                raise ValueError("Either noteid or notetitle must be provided")

    def create_note(
        self,
        title="CLI Note",
        parent_id: typing.Optional[str] = None,
        order=None,
        checked=None,
        icon=None,
    ) -> Note:
        self.debug("Creating Note")
        if title is None:
            raise ValueError("Note title must not be null.")
        note = self.post(
            self.base_endpoint,
            json={
                "order": order,
                "parent": parent_id or None,
                "checked": checked,
                "title": title,
            },
        ).json()
        self.debug(f"We created note with {note}")
        if icon:
            self.set_icon(note.get("id"), icon)
        elif title == "Uploads":
            self.set_icon(note.get("id"), "📤")
        return Note(note)

    def delete_note(self, notes_id: str):
        url = urljoin(self.base_endpoint, f"{notes_id}/")
        self.delete(url)

    def set_icon(self, notes_id: str, icon: str):
        url = urljoin(self.base_endpoint, f"{notes_id}/")
        self.put(url, json={"icon_emoji": icon})

    def _upload_note(
        self,
        note: Note,
    ):
        self.debug(f"Note has length: {len(note.text)}")
        url = urljoin(self.base_endpoint, note.id, "")
        r = self.put(url, json=note.to_dict())

        try:
            r.raise_for_status()
            self.display(f'Note written to "{note.title}".')
        except HTTPError as e:
            raise HTTPError(
                f'{str(e)} Are you uploading binary content to note? (Try "file" subcommand)'
            ) from e

    def write_note_templates(
        self,
        note_templates: typing.Union[NoteTemplate, typing.List[NoteTemplate]],
        timestamp: bool = True,
        **kwargs,
    ):
        if not isinstance(note_templates, list):
            note_templates = [note_templates]
        for note_template in note_templates:
            if note_template.parent_notetitle and not note_template.parent:
                note_template.parent = self.get_or_create_note_by_title(
                    note_template.parent_notetitle
                ).id

            note = None
            if not note_template.force_new:
                note = self.get_note_by_title(
                    note_template.title,
                    parent_noteid=note_template.parent,
                )
            if note is None:
                new_note = True
                note = self.create_note(
                    title=note_template.title,
                    parent_id=note_template.parent or None,
                )
            else:
                new_note = False
            self.debug(f"Got note from server {note.title} ({note.id})")

            # Prepare note (append content to existing note, etc)
            note_text = note.text + "\n\n" if note.text else ""
            if (
                timestamp and note_template.text
            ):  # Only add timestamp if there is content
                note_text += f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
                if "\n" in note_template.text:
                    if not note_template.text.startswith("\n"):
                        note_text += "\n"
                else:
                    note_text += ": "
            note_text += note_template.text or ""

            if new_note:
                upload_note = Note.from_note_template(note_template)
                upload_note.parent = note.parent
                upload_note.id = note.id
                upload_note.text = note_text
            else:
                upload_note = note
                upload_note.text = note_text

            # Upload note and children recursively
            self._upload_note(upload_note, **kwargs)
            for child in note_template.children:
                child.parent = note.id
                self.write_note_templates(child, timestamp=timestamp, **kwargs)

    def write_note(
        self,
        timestamp: bool = False,
        **kwargs,
    ):
        note_template = NoteTemplate.from_kwargs(**kwargs)
        self.write_note_templates(
            note_template, timestamp=timestamp
        )

    def get_note_by_title(
        self,
        title,
        parent_noteid=None,  # Preferred over parent_notetitle
        parent_notetitle=None,
        ignore_parent=False,
    ) -> typing.Optional[Note]:
        if not parent_noteid and parent_notetitle:
            try:
                parent_noteid = self.get_note_by_title(parent_notetitle, ignore_parent=True).id  # type: ignore
            except AttributeError:
                raise ValueError(f'Parent note "{parent_notetitle}" does not exist.')
        notes_list = self.get_notes()

        for note in notes_list:
            if note.title == title and (note.parent == parent_noteid or ignore_parent):
                break
        else:
            return None
        return note

    def get_or_create_note_by_title(
        self,
        title,
        parent_noteid=None,  # Preferred over parent_notetitle
        parent_notetitle=None,
        icon=None,
    ) -> Note:
        if not parent_noteid and parent_notetitle:
            parent_noteid = self.get_or_create_note_by_title(
                parent_notetitle, icon=icon
            ).id
        note = self.get_note_by_title(title, parent_noteid=parent_noteid)
        if not note:
            # Note does not exist. Create.
            note = self.create_note(title=title, parent_id=parent_noteid, icon=icon)
        return note

    def upload_file(
        self,
        file: typing.Optional[typing.IO] = None,
        content: typing.Optional[bytes] = None,
        notetitle: typing.Optional[str] = None,
        filename: typing.Optional[str] = None,
        caption: typing.Optional[str] = None,
        parent_notetitle: typing.Optional[str] = None,
        **kwargs,
    ):
        assert file or content
        assert not (file and content)
        if notetitle is None:
            notetitle = "Uploads"

        if file:
            if file.name == "<stdin>":
                self.display("Reading from stdin...")
            elif not filename:
                filename = basename(file.name)
            # TODO this might be streamed to not load entire file to memory
            try:
                content = file.buffer.read()  # type: ignore
            except AttributeError:
                content = file.read()
            if not filename:
                filetype = guess_filetype(content) or "dat"
                filename = f"data.{filetype}"

            if not content:
                self.warning(f"{file.name} is empty. Will not upload.")
                return

        self.get_or_create_note_by_title(
            notetitle, parent_notetitle=parent_notetitle
        )
        if self.private_note:
            url = urljoin(self.base_endpoint, "upload/")
        else:
            url = urljoin(self.base_endpoint.rsplit("/", 2)[0], "upload/")
        response_json = self.post(
            url, files={"file": (filename, content)}, json_content=False
        ).json()
        is_image = True if response_json.get("resource_type") == "image" else False
        if is_image:
            file_path = f"/images/name/{response_json['name']}"
            note_content = f"\n![{caption or filename}]({file_path})"
        else:
            file_path = f"/files/name/{response_json['name']}"
            note_content = f"\n[{caption or filename}]({file_path})"

        self.write_note(
            title=notetitle,
            text=note_content,
            parent_notetitle=parent_notetitle,
            **kwargs,
        )
