# -*- coding: utf-8 -*-
import base64
import email
import imaplib
import logging
import re
from email.header import decode_header
from email.utils import getaddresses, parsedate_to_datetime, parseaddr

from odoo import _, api, fields, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

# Batch size per cron run to avoid IMAP timeouts on large mailboxes.
FETCH_BATCH_SIZE = 50


def _decode_mime_header(value):
    """Decode RFC 2047 encoded mail headers to a plain string."""
    if not value:
        return ''
    parts = []
    for fragment, charset in decode_header(value):
        if isinstance(fragment, bytes):
            parts.append(fragment.decode(charset or 'utf-8', errors='replace'))
        else:
            parts.append(fragment)
    return ''.join(parts)


def _extract_addresses(header_value):
    """Return comma-separated display addresses from a mail header."""
    if not header_value:
        return ''
    addresses = getaddresses([header_value])
    formatted = []
    for name, addr in addresses:
        if not addr:
            continue
        if name:
            formatted.append(f'{name} <{addr}>')
        else:
            formatted.append(addr)
    return ', '.join(formatted)


def _extract_email_addresses(header_value):
    """Return a list of lowercase email addresses from a mail header."""
    if not header_value:
        return []
    return [
        addr.lower()
        for _name, addr in getaddresses([header_value])
        if addr
    ]


def _parse_email_body(msg):
    """Extract plain-text and HTML body parts from an email.message."""
    body_text = ''
    body_html = ''
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            disposition = str(part.get('Content-Disposition', ''))
            if 'attachment' in disposition:
                continue
            payload = part.get_payload(decode=True)
            if not payload:
                continue
            charset = part.get_content_charset() or 'utf-8'
            try:
                decoded = payload.decode(charset, errors='replace')
            except (LookupError, UnicodeDecodeError):
                decoded = payload.decode('utf-8', errors='replace')
            if content_type == 'text/plain' and not body_text:
                body_text = decoded
            elif content_type == 'text/html' and not body_html:
                body_html = decoded
    else:
        payload = msg.get_payload(decode=True)
        if payload:
            charset = msg.get_content_charset() or 'utf-8'
            try:
                decoded = payload.decode(charset, errors='replace')
            except (LookupError, UnicodeDecodeError):
                decoded = payload.decode('utf-8', errors='replace')
            if msg.get_content_type() == 'text/html':
                body_html = decoded
            else:
                body_text = decoded
    if not body_text and body_html:
        # Fallback: strip tags crudely for search/rules in phase 2.
        body_text = re.sub(r'<[^>]+>', ' ', body_html)
    return body_text, body_html


class MailSyncServer(models.Model):
    _name = 'mail.sync.server'
    _description = 'IMAP Mail Sync Server'
    _order = 'name'

    name = fields.Char(string='Name', required=True)
    server = fields.Char(string='IMAP Server', required=True)
    port = fields.Integer(string='Port', default=993, required=True)
    is_ssl = fields.Boolean(string='Use SSL', default=True)
    user = fields.Char(string='Username', required=True)
    password = fields.Char(
        string='Password',
        required=True,
        groups='jb_mail_inbox.group_mail_inbox_manager,base.group_system',
    )
    folder = fields.Char(string='Folder', default='INBOX', required=True)
    last_uid = fields.Integer(
        string='Last UID',
        default=0,
        help='Last successfully fetched IMAP UID. Used to avoid re-importing.',
    )
    initial_fetch_unseen_only = fields.Boolean(
        string='First Fetch: Unseen Only',
        default=True,
        help='On first sync (last UID is 0), fetch only UNSEEN messages.',
    )
    active = fields.Boolean(default=True)
    state = fields.Selection(
        [
            ('draft', 'Not Tested'),
            ('ok', 'Connected'),
            ('error', 'Error'),
        ],
        string='Connection Status',
        default='draft',
        readonly=True,
    )
    last_error = fields.Text(string='Last Error', readonly=True)
    last_fetch_date = fields.Datetime(string='Last Fetch', readonly=True)
    message_count = fields.Integer(
        string='Messages',
        compute='_compute_message_count',
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env.company,
    )
    crm_route_enabled = fields.Boolean(
        string='Route to CRM Leads',
        default=True,
        help='Post synced mail as log notes on matching CRM leads.',
    )
    crm_create_lead_if_unmatched = fields.Boolean(
        string='CRM: Create Lead if Unmatched',
        default=True,
        help='Create a new CRM lead when no existing lead matches the sender email.',
    )

    @api.depends('name')
    def _compute_message_count(self):
        grouped = self.env['mail.sync.message'].read_group(
            [('server_id', 'in', self.ids)],
            ['server_id'],
            ['server_id'],
        )
        counts = {row['server_id'][0]: row['server_id_count'] for row in grouped}
        for server in self:
            server.message_count = counts.get(server.id, 0)

    def _connect_imap(self):
        """Open and authenticate an IMAP connection."""
        self.ensure_one()
        port = self.port or (993 if self.is_ssl else 143)
        try:
            if self.is_ssl:
                conn = imaplib.IMAP4_SSL(self.server, port)
            else:
                conn = imaplib.IMAP4(self.server, port)
            conn.login(self.user, self.password)
            return conn
        except imaplib.IMAP4.error as exc:
            raise UserError(_('IMAP login failed: %s') % exc) from exc
        except OSError as exc:
            raise UserError(_('Could not connect to IMAP server: %s') % exc) from exc

    def action_test_connection(self):
        """Verify IMAP credentials and folder access."""
        self.ensure_one()
        conn = None
        try:
            conn = self._connect_imap()
            status, _data = conn.select(self.folder, readonly=True)
            if status != 'OK':
                raise UserError(_('Could not select folder "%s".') % self.folder)
            self.write({'state': 'ok', 'last_error': False})
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _('Connection Successful'),
                    'message': _('IMAP connection to %s is working.') % self.name,
                    'type': 'success',
                    'sticky': False,
                },
            }
        except UserError as exc:
            self.write({'state': 'error', 'last_error': str(exc)})
            raise
        except Exception as exc:
            self.write({'state': 'error', 'last_error': str(exc)})
            raise UserError(_('Connection test failed: %s') % exc) from exc
        finally:
            if conn:
                try:
                    conn.logout()
                except Exception:
                    pass

    def action_fetch_now(self):
        """Manually trigger mail fetch for selected servers."""
        self.fetch_mail()
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Fetch Complete'),
                'message': _('Mail fetch finished for %s server(s).') % len(self),
                'type': 'success',
                'sticky': False,
            },
        }

    @api.model
    def _cron_fetch_all_servers(self):
        """Cron entry point: fetch mail from all active servers."""
        servers = self.search([('active', '=', True)])
        if servers:
            servers.fetch_mail()

    def fetch_mail(self):
        """Fetch new messages from IMAP for each server."""
        for server in self:
            try:
                server._fetch_mail_single()
            except Exception as exc:
                _logger.exception('Mail fetch failed for server %s', server.name)
                server.write({'state': 'error', 'last_error': str(exc)})

    def _fetch_mail_single(self):
        """Fetch and import messages for one IMAP server."""
        self.ensure_one()
        conn = None
        imported = 0
        try:
            conn = self._connect_imap()
            status, _data = conn.select(self.folder, readonly=True)
            if status != 'OK':
                raise UserError(_('Could not select folder "%s".') % self.folder)

            uid_list = self._search_uids(conn)
            if not uid_list:
                self.write({
                    'state': 'ok',
                    'last_error': False,
                    'last_fetch_date': fields.Datetime.now(),
                })
                return

            # Process oldest first so last_uid advances correctly.
            uid_list = sorted(int(uid) for uid in uid_list)
            max_uid = self.last_uid

            for uid in uid_list[:FETCH_BATCH_SIZE]:
                if self._import_uid(conn, uid):
                    imported += 1
                max_uid = max(max_uid, uid)

            self.write({
                'last_uid': max_uid,
                'state': 'ok',
                'last_error': False,
                'last_fetch_date': fields.Datetime.now(),
            })
            _logger.info(
                'Fetched %d message(s) from %s (last_uid=%s)',
                imported, self.name, max_uid,
            )
        finally:
            if conn:
                try:
                    conn.logout()
                except Exception:
                    pass

    def _search_uids(self, conn):
        """Return IMAP UIDs to fetch based on last_uid and first-run settings."""
        self.ensure_one()
        if self.last_uid:
            status, data = conn.uid('search', None, 'UID', '%d:*' % (self.last_uid + 1))
        elif self.initial_fetch_unseen_only:
            status, data = conn.uid('search', None, 'UNSEEN')
        else:
            status, data = conn.uid('search', None, 'ALL')

        if status != 'OK' or not data or not data[0]:
            return []
        uids = data[0].split()
        if self.last_uid:
            uids = [uid for uid in uids if int(uid) > self.last_uid]
        return uids

    def _import_uid(self, conn, uid):
        """Fetch one UID and create a mail.sync.message if not duplicate."""
        self.ensure_one()
        Message = self.env['mail.sync.message']
        if Message.search_count([('server_id', '=', self.id), ('uid', '=', uid)]):
            return False

        status, data = conn.uid('fetch', str(uid), '(RFC822)')
        if status != 'OK' or not data or not data[0]:
            return False

        raw_bytes = data[0][1]
        if not raw_bytes:
            return False

        msg = email.message_from_bytes(raw_bytes)
        message_id = (msg.get('Message-ID') or '').strip()
        if message_id and Message.search_count([
            ('server_id', '=', self.id),
            ('message_id', '=', message_id),
        ]):
            return False

        subject = _decode_mime_header(msg.get('Subject', ''))
        email_from = _extract_addresses(msg.get('From', '')) or parseaddr(msg.get('From', ''))[1]
        email_to = _extract_addresses(msg.get('To', ''))
        email_cc = _extract_addresses(msg.get('Cc', ''))
        email_bcc = _extract_addresses(msg.get('Bcc', ''))
        body_text, body_html = _parse_email_body(msg)

        date_received = fields.Datetime.now()
        date_header = msg.get('Date')
        if date_header:
            try:
                date_received = parsedate_to_datetime(date_header)
                if date_received.tzinfo:
                    date_received = date_received.replace(tzinfo=None)
            except (TypeError, ValueError, OverflowError):
                pass

        sync_msg = Message.create({
            'server_id': self.id,
            'uid': uid,
            'message_id': message_id or False,
            'email_from': email_from,
            'email_to': email_to,
            'email_cc': email_cc,
            'email_bcc': email_bcc,
            'subject': subject or _('(No Subject)'),
            'body_text': body_text,
            'body_html': body_html,
            'date_received': date_received,
            'raw_headers': '\n'.join(f'{k}: {v}' for k, v in msg.items()),
            'state': 'new',
        })
        self._create_attachments(sync_msg, msg)
        sync_msg._route_all()
        return True

    def _create_attachments(self, sync_msg, msg):
        """Extract file attachments from the email and link to the message."""
        Attachment = self.env['ir.attachment']
        for part in msg.walk():
            disposition = str(part.get('Content-Disposition', ''))
            if 'attachment' not in disposition and not part.get_filename():
                continue
            filename = part.get_filename()
            if filename:
                filename = _decode_mime_header(filename)
            else:
                filename = 'attachment'
            payload = part.get_payload(decode=True)
            if not payload:
                continue
            attachment = Attachment.create({
                'name': filename,
                'type': 'binary',
                'datas': base64.b64encode(payload),
                'res_model': 'mail.sync.message',
                'res_id': sync_msg.id,
                'mimetype': part.get_content_type(),
            })
            sync_msg.attachment_ids = [(4, attachment.id)]
