#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>

from html import escape
from math import ceil
from typing import NamedTuple

from qt.core import (
    QFont,
    QFontInfo,
    QFrame,
    QHBoxLayout,
    QIcon,
    QLabel,
    QPalette,
    QSize,
    QSizePolicy,
    Qt,
    QTextBrowser,
    QTextEdit,
    QToolButton,
    QUrl,
    QVBoxLayout,
    QWidget,
    pyqtSignal,
)

from calibre.utils.config_base import tweaks
from calibre.utils.logging import INFO, WARN
from calibre.utils.resources import get_image_path


class Browser(QTextBrowser):

    def __init__(self, parent: QWidget = None):
        super().__init__(parent)
        self.setOpenLinks(False)
        self.setMinimumHeight(150)
        self.setFrameShape(QFrame.Shape.NoFrame)
        self.setContentsMargins(0, 0, 0, 0)
        self.document().setDocumentMargin(0)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        if tweaks['change_ai_chat_font_size_by']:
            font = QFont(self.font())
            font.setPixelSize(QFontInfo(font).pixelSize() + tweaks['change_ai_chat_font_size_by'])
            self.setFont(font)

    def sizeHint(self) -> QSize:
        return QSize(600, 500)

    def setHtml(self, html: str) -> None:
        super().setHtml(html)
        self.document().setDocumentMargin(0)

    def scroll_to_bottom(self) -> None:
        self.verticalScrollBar().setValue(self.verticalScrollBar().maximum())


class Button(NamedTuple):
    icon: str
    link: str
    tooltip: str = ''

    def as_html(self, line_spacing: int) -> str:
        path = get_image_path(self.icon)
        url = QUrl.fromLocalFile(path).toString(QUrl.ComponentFormattingOption.FullyEncoded)
        sz = line_spacing - 2
        return f'''<a style="text-decoration: none" href="{escape(self.link)}" title="{escape(self.tooltip)}"
><img height="{sz}" width="{sz}" src="{url}"></a>'''


class Header(NamedTuple):
    title: str = ''
    buttons: tuple[Button, ...] = ()

    def as_html(self, line_spacing: int) -> str:
        links = '\xa0\xa0'.join(b.as_html(line_spacing) for b in self.buttons)
        title = '<b><i>' + escape(self.title)
        if links:
            return f'''<table width="100%" cellpadding="0" cellspacing="0"><tr><td>{title}\xa0</td>
                    <td style="text-align: right">{links}</td></tr></table>'''
        return f'<div>{title}</div>'


class InputEdit(QTextEdit):

    returnPressed = pyqtSignal()

    def __init__(self, parent: QWidget = None, placeholder_text: str = ''):
        super().__init__(parent)
        self.setPlaceholderText(placeholder_text)
        self.height_for_frame = 2 * self.frameWidth() + self.contentsMargins().top() + self.contentsMargins().bottom()
        self.line_height = ceil(self.fontMetrics().lineSpacing())
        self.maximum_height = 3 * self.line_height + self.height_for_frame
        self.min_height = self.line_height + self.height_for_frame
        self.textChanged.connect(self.adjust_height)
        self.adjust_height()

    def calculate_single_line_height(self) -> int:
        line_height = self.fontMetrics().lineSpacing()
        return ceil(line_height + self.height_for_frame)

    def adjust_height(self) -> None:
        doc_height = ceil(self.document().size().height())
        self.setFixedHeight(max(self.min_height, min(doc_height + self.height_for_frame, self.maximum_height)))
        self.ensureCursorVisible()

    def set_max_height(self, val: int) -> None:
        self.maximum_height = val
        self.adjust_height()

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
            mods = event.modifiers() & (
                Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier |
                Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
            if mods in (Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.ControlModifier):
                self.returnPressed.emit()
                return
        super().keyPressEvent(event)

    @property
    def value(self) -> str:
        return self.toPlainText()

    @value.setter
    def value(self, val: str) -> None:
        self.setPlainText(val)


class Input(QWidget):

    send_requested = pyqtSignal()

    def __init__(self, parent: QWidget = None, placeholder_text: str = ''):
        super().__init__(parent)
        l = QHBoxLayout(self)
        l.setContentsMargins(0, 0, 0, 0)
        self.text_input = ti = InputEdit(self, placeholder_text)
        ti.returnPressed.connect(self.send_requested)
        l.addWidget(ti)
        self.send_button = b = QToolButton(self)
        b.setIcon(QIcon.ic('send.png'))
        b.setToolTip(_('Send query to AI'))
        b.clicked.connect(self.send_requested)
        l.addWidget(b, alignment=Qt.AlignmentFlag.AlignCenter)

    def setFocus(self, reason) -> None:
        self.text_input.setFocus(reason)

    @property
    def value(self) -> str:
        return self.text_input.value

    @value.setter
    def value(self, val: str) -> None:
        self.text_input.value = val

    def set_max_height(self, val: int) -> None:
        self.text_input.set_max_height(val)


class ChatWidget(QWidget):

    link_clicked = pyqtSignal(QUrl)
    input_from_user = pyqtSignal(str)

    def __init__(self, parent: QWidget = None, placeholder_text: str = '', disclaimer_text: str | None = None):
        super().__init__(parent)
        if disclaimer_text is None:
            disclaimer_text = _(
                'AI generated answers can be inaccurate, please verify any answers before acting on them.')
        self.disclaimer_text = disclaimer_text
        l = QVBoxLayout(self)
        l.setContentsMargins(0, 0, 0, 0)
        self.browser = b = Browser(self)
        b.anchorClicked.connect(self.link_clicked)
        l.addWidget(b)
        self.blocks: list[str] = []
        self.current_message = ''
        pal = self.palette()
        self.response_color = pal.color(QPalette.ColorRole.Window).name()
        self.base_color = pal.color(QPalette.ColorRole.Base).name()
        self.disclaimer_label = dl = QLabel(self.disclaimer_text, self)
        dl.setWordWrap(True)
        dl.setTextFormat(Qt.TextFormat.PlainText)
        dl.setMargin(2)
        font = QFont(dl.font())
        font.setItalic(True)
        dl.setFont(font)
        dl.setStyleSheet(f'background-color: {self.base_color};')
        l.addWidget(dl)
        self.input = iw = Input(parent=self, placeholder_text=placeholder_text)
        iw.send_requested.connect(self.on_input)
        l.addWidget(iw)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.line_spacing = self.browser.fontMetrics().lineSpacing()
        self.disclaimer_label.setVisible(False)

    def setFocus(self, reason) -> None:
        self.input.setFocus(reason)

    def wrap_content_in_padding_table(self, html: str, background_color: str = '') -> str:
        style = f'style="background-color: {background_color}"' if background_color else ''
        return f'''<table width="100%" {style} cellpadding="2"><tr><td>{html}</td></tr></table>'''

    # API {{{
    def add_block(self, body_html: str, header: Header = Header(), is_response: bool = False) -> None:
        self.current_message = ''
        html = ''
        if header.title or header.buttons:
            html += f'<div>{header.as_html(self.line_spacing)}</div>'
        html += f'<div>{body_html}</div>'
        bg = self.response_color if is_response else self.base_color
        self.blocks.append(self.wrap_content_in_padding_table(html, bg))

    def show_message(self, msg_html: str, details: str = '', level: int = INFO, clear_conversation: bool = True) -> None:
        if clear_conversation:
            self.blocks = []
        style = ''
        if level == WARN:
            style += 'color: orange;'
        elif level > WARN:
            style += 'color: red;'
        html = f'<div style="{style}">{msg_html}</div>'
        if details:
            html += f"<pre>{_('Details:')}\n{escape(details)}</pre>"
        self.current_message = self.wrap_content_in_padding_table(html)
        self.re_render()

    def clear(self) -> None:
        self.current_message = ''
        self.blocks = []
        self.re_render()

    def set_input_enabled(self, enabled: bool) -> None:
        self.input.setEnabled(enabled)

    def scroll_to_bottom(self) -> None:
        self.browser.scroll_to_bottom()
    # }}}

    def resizeEvent(self, ev) -> None:
        super().resizeEvent(ev)
        self.input.set_max_height(ceil(self.height() * 0.25))

    def re_render(self) -> None:
        self.disclaimer_label.setVisible(not self.current_message and bool(self.blocks))
        if self.current_message:
            self.browser.setHtml(self.current_message)
        else:
            html = '\n\n'.join(self.blocks)
            self.browser.setHtml(html)

    def on_input(self) -> None:
        text = self.input.value
        self.input.value = ''
        self.input_from_user.emit(text)
