"""
BizNode Email Inbox Service
============================
Polls the AI agent's own IMAP inbox for owner reply emails,
parses them with the LLM, and executes instructions such as:
  - block <telegram_id>
  - unblock <telegram_id>
  - add: <new business knowledge>
  - delete: <note title>

All instructions are logged to the email_instructions table.
"""

from __future__ import annotations

import email as _email_lib
import imaplib
import importlib.util
import json
import logging
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

log = logging.getLogger(__name__)

_PROJECT_ROOT = Path(__file__).resolve().parent.parent

# ---------------------------------------------------------------------------
# IMAP configuration  (falls back to SMTP credentials if not set separately)
# ---------------------------------------------------------------------------
IMAP_HOST     = os.getenv("IMAP_HOST", "imap.gmail.com")
IMAP_PORT     = int(os.getenv("IMAP_PORT", "993"))
IMAP_USER     = os.getenv("IMAP_USER") or os.getenv("SMTP_USER", "")
IMAP_PASSWORD = os.getenv("IMAP_PASSWORD") or os.getenv("SMTP_PASSWORD", "")
OWNER_EMAIL   = os.getenv("OWNER_EMAIL", "")


# ---------------------------------------------------------------------------
# DB loader
# ---------------------------------------------------------------------------
def _load_db():
    db_path = _PROJECT_ROOT / "memory" / "database.py"
    spec = importlib.util.spec_from_file_location("_db", str(db_path))
    mod  = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ---------------------------------------------------------------------------
# IMAP helpers
# ---------------------------------------------------------------------------

def _decode_header(raw_header: str) -> str:
    """Decode a possibly RFC2047-encoded email header to plain text."""
    parts = _email_lib.header.decode_header(raw_header)
    decoded = []
    for part, charset in parts:
        if isinstance(part, bytes):
            decoded.append(part.decode(charset or "utf-8", errors="replace"))
        else:
            decoded.append(str(part))
    return " ".join(decoded)


def _extract_body(msg) -> str:
    """Walk a parsed email message and return plain-text body."""
    body_parts = []
    if msg.is_multipart():
        for part in msg.walk():
            ct = part.get_content_type()
            if ct == "text/plain":
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or "utf-8"
                    body_parts.append(payload.decode(charset, errors="replace"))
    else:
        payload = msg.get_payload(decode=True)
        if payload:
            charset = msg.get_content_charset() or "utf-8"
            body_parts.append(payload.decode(charset, errors="replace"))
    return "\n".join(body_parts).strip()


def _verify_sender(msg, owner_email: str) -> bool:
    """
    Verify that the email sender actually matches the owner.

    Checks:
      1. From header matches OWNER_EMAIL (basic — can be spoofed)
      2. Authentication-Results / ARC-Authentication-Results headers
         for SPF pass, DKIM pass, or DMARC pass (if present)
      3. Return-Path / Envelope-From consistency

    If the mail server provides authentication headers (Gmail, Outlook,
    etc. do), a spoofed email will show spf=fail / dkim=fail and be
    rejected here.  If no auth headers are present (self-hosted MTA
    without milter), we fall back to From-header matching only but log
    a warning.
    """
    if not owner_email:
        return False

    # ── 1. Check From header ──────────────────────────────────────────────
    from_header = msg.get("From", "")
    # Extract the email address from "Name <addr>" or bare "addr"
    from_addr = ""
    if "<" in from_header and ">" in from_header:
        from_addr = from_header.split("<")[1].split(">")[0].strip().lower()
    else:
        from_addr = from_header.strip().lower()

    if from_addr != owner_email.lower():
        log.warning("SENDER_MISMATCH from=%s expected=%s — rejected", from_addr, owner_email)
        return False

    # ── 2. Check Authentication-Results (SPF / DKIM / DMARC) ─────────────
    auth_headers = []
    for hdr_name in ("Authentication-Results", "ARC-Authentication-Results"):
        vals = msg.get_all(hdr_name)
        if vals:
            auth_headers.extend(vals)

    if auth_headers:
        combined = " ".join(h.lower() for h in auth_headers)

        # Reject if explicit SPF or DKIM failure is detected
        spf_fail = "spf=fail" in combined or "spf=softfail" in combined
        dkim_fail = "dkim=fail" in combined
        dmarc_fail = "dmarc=fail" in combined

        if spf_fail and dkim_fail:
            log.warning("SPOOFED_EMAIL detected: SPF and DKIM both failed for %s", from_addr)
            return False
        if dmarc_fail:
            log.warning("SPOOFED_EMAIL detected: DMARC failed for %s", from_addr)
            return False
        if spf_fail and "dkim=pass" not in combined:
            log.warning("SPOOFED_EMAIL suspected: SPF failed, no DKIM pass for %s", from_addr)
            return False

        log.debug("Sender %s passed authentication checks", from_addr)
    else:
        log.warning("NO_AUTH_HEADERS for email from %s — accepting on From-header match only", from_addr)

    # ── 3. Check Return-Path consistency ──────────────────────────────────
    return_path = (msg.get("Return-Path") or "").strip("<>").strip().lower()
    if return_path and return_path != from_addr:
        # Return-Path mismatch is suspicious but not always spoofing
        # (mailing lists, forwarding). Log but allow if auth headers passed.
        if not auth_headers:
            log.warning("RETURN_PATH_MISMATCH return_path=%s from=%s — rejected (no auth headers)",
                        return_path, from_addr)
            return False
        log.info("Return-Path differs (%s vs %s) but auth headers present — allowing", return_path, from_addr)

    return True


def fetch_unread_owner_replies() -> List[Dict]:
    """
    Connect to IMAP, fetch unread emails from OWNER_EMAIL, mark them read.
    Returns list of {uid, subject, body, date}.

    Each email is verified against SPF/DKIM/DMARC authentication headers
    to prevent spoofed emails from being processed as owner instructions.
    """
    if not IMAP_USER or not IMAP_PASSWORD:
        log.warning("IMAP credentials not configured — skipping inbox check")
        return []

    results = []
    try:
        mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
        mail.login(IMAP_USER, IMAP_PASSWORD)
        mail.select("INBOX")

        # Search for unseen emails from the owner
        search_criterion = "UNSEEN"
        if OWNER_EMAIL:
            search_criterion = f'(UNSEEN FROM "{OWNER_EMAIL}")'

        status, data = mail.search(None, search_criterion)
        if status != "OK" or not data[0]:
            mail.logout()
            return []

        uids = data[0].split()
        for uid in uids:
            try:
                _, msg_data = mail.fetch(uid, "(RFC822)")
                raw = msg_data[0][1]
                msg = _email_lib.message_from_bytes(raw)

                # Verify sender authenticity before processing
                if not _verify_sender(msg, OWNER_EMAIL):
                    log.warning("Rejected email uid %s — sender verification failed", uid)
                    mail.store(uid, "+FLAGS", "\\Seen")  # Mark read so we don't re-check
                    continue

                subject = _decode_header(msg.get("Subject", ""))
                body    = _extract_body(msg)
                date    = msg.get("Date", "")

                results.append({"uid": uid.decode(), "subject": subject, "body": body, "date": date})

                # Mark as read
                mail.store(uid, "+FLAGS", "\\Seen")
            except Exception as exc:
                log.warning("Could not process email uid %s: %s", uid, exc)

        mail.logout()
    except Exception as exc:
        log.error("IMAP fetch failed: %s", exc)

    return results


# ---------------------------------------------------------------------------
# Instruction parser (LLM-based)
# ---------------------------------------------------------------------------

_PARSE_PROMPT = """You are BizNode's email instruction parser.

The business owner sent an instruction email. Extract ALL instructions.
Return ONLY a valid JSON array. Each element has:
  "action"  : one of "block" | "unblock" | "add_knowledge" | "delete_knowledge"
              | "memory" | "task" | "client" | "note"
  "target"  : telegram_id (block/unblock), note title (delete_knowledge),
               memory title (memory), task name (task), client name (client), else ""
  "content" : reason / knowledge text / memory body / task definition / client fields JSON, else ""

Rules:
  block 123456789 reason: spam       → {action:"block",target:"123456789",content:"spam"}
  unblock 987654321                  → {action:"unblock",target:"987654321",content:""}
  add: Our pricing is $50/hr         → {action:"add_knowledge",target:"",content:"Our pricing is $50/hr"}
  delete: Old Pricing 2025           → {action:"delete_knowledge",target:"Old Pricing 2025",content:""}
  memory: Title | body text          → {action:"memory",target:"Title",content:"body text"}
  task: Name | cron | type | detail  → {action:"task",target:"Name",content:"cron|type|detail"}
  client:\\nname: X\\nemail: y\\n...   → {action:"client",target:"X",content:"{\"name\":\"X\",\"email\":\"y\"}"}

Email body:
---
{body}
---

Return ONLY the JSON array, no explanation."""


def parse_instruction(email_body: str) -> List[Dict]:
    """
    Use LLM to parse owner email body into a list of structured instructions.
    Falls back to a single "note" action on any error.
    """
    try:
        from services.llm_service import ask_llm
        prompt   = _PARSE_PROMPT.replace("{body}", email_body[:3000])
        raw      = ask_llm(prompt)
        # Extract JSON from the LLM response (may have extra text)
        start    = raw.find("[")
        end      = raw.rfind("]") + 1
        if start == -1 or end == 0:
            raise ValueError("No JSON array in LLM response")
        parsed   = json.loads(raw[start:end])
        if isinstance(parsed, list):
            return parsed
    except Exception as exc:
        log.warning("Instruction parse failed: %s", exc)
    return [{"action": "note", "target": "", "content": email_body[:500]}]


# ---------------------------------------------------------------------------
# Instruction executor
# ---------------------------------------------------------------------------

def execute_instruction(parsed_list: List[Dict]) -> str:
    """Execute a list of parsed instructions. Returns human-readable result summary."""
    db      = _load_db()
    results = []

    for item in parsed_list:
        action  = item.get("action", "note")
        target  = str(item.get("target", "")).strip()
        content = str(item.get("content", "")).strip()

        try:
            if action == "block" and target:
                db.block_customer(target, reason=content, blocked_by="email_instruction")
                results.append(f"Blocked {target}" + (f" — {content}" if content else ""))

            elif action == "unblock" and target:
                db.unblock_customer(target)
                results.append(f"Unblocked {target}")

            elif action == "add_knowledge" and content:
                node_id = f"email_{uuid.uuid4().hex[:10]}"
                title   = target or f"Email Import {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
                try:
                    obsidian_path = str(_PROJECT_ROOT / "memory" / "obsidian_layer.py")
                    spec = importlib.util.spec_from_file_location("_obs", obsidian_path)
                    mod  = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(mod)
                    mod.AIObsidianLayer().write_memory(node_id=node_id, title=title, content=content)
                    results.append(f"Added knowledge: '{title}'")
                except Exception as exc:
                    results.append(f"add_knowledge failed: {exc}")

            elif action == "delete_knowledge" and target:
                notes = db.get_all_notes()
                match = next(
                    (n for n in notes if target.lower() in (n.get("title") or "").lower()), None
                )
                if match:
                    db.delete_note(match["id"])
                    results.append(f"Deleted note: '{match['title']}'")
                else:
                    results.append(f"Note not found: '{target}'")

            elif action == "memory" and (target or content):
                title   = target or f"Memory {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}"
                body    = content or target
                node_id = f"memory_{uuid.uuid4().hex[:10]}"
                try:
                    obsidian_path = str(_PROJECT_ROOT / "memory" / "obsidian_layer.py")
                    spec = importlib.util.spec_from_file_location("_obs2", obsidian_path)
                    mod  = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(mod)
                    mod.AIObsidianLayer().write_memory(node_id=node_id, title=title, content=body)
                    results.append(f"Memory stored: '{title}'")
                except Exception as exc:
                    try:
                        db.create_note(node_id=node_id, title=title, content=body,
                                       summary="", tags="email_memory")
                        results.append(f"Memory note created: '{title}'")
                    except Exception as exc2:
                        results.append(f"memory failed: {exc2}")

            elif action == "task" and target:
                parts = [p.strip() for p in (content or "").split("|")]
                cron_expr   = parts[0] if len(parts) > 0 else "0 9 * * 1"
                action_type = parts[1] if len(parts) > 1 else "note"
                detail      = parts[2] if len(parts) > 2 else content
                task_id     = f"email_{uuid.uuid4().hex[:8]}"
                action_json = json.dumps({
                    "type": action_type, "detail": detail,
                    "created_via": "email_instruction",
                })
                try:
                    db.create_scheduled_task(
                        id=task_id, name=target, type=action_type,
                        action_json=action_json, schedule_cron=cron_expr,
                    )
                    results.append(f"Task created: '{target}' cron={cron_expr}")
                except Exception as exc:
                    results.append(f"task failed: {exc}")

            elif action == "client" and target:
                fields: dict = {}
                try:
                    fields = json.loads(content)
                except Exception:
                    for line in content.splitlines():
                        if ":" in line:
                            k, _, v = line.partition(":")
                            fields[k.strip().lower()] = v.strip()
                fields["name"]   = fields.get("name") or target
                fields["source"] = "email_instruction"
                try:
                    client_id = db.create_client_file(
                        name           = fields.get("name", target),
                        email          = fields.get("email", ""),
                        phone          = fields.get("phone", ""),
                        company        = fields.get("company", ""),
                        website        = fields.get("website", ""),
                        address        = fields.get("address", ""),
                        telegram_id    = fields.get("telegram_id", ""),
                        notes          = fields.get("notes", ""),
                        tags           = fields.get("tags", ""),
                        status         = fields.get("status", "active"),
                        source         = "email_instruction",
                        contract_value = float(fields["contract_value"]) if fields.get("contract_value") else None,
                    )
                    results.append(f"Client file created: '{fields.get('name', target)}' (id={client_id})")
                except Exception as exc:
                    results.append(f"client failed: {exc}")

            elif action == "note":
                results.append("Logged as note (no action taken)")

            else:
                results.append(f"Unknown action or missing target: {action}")

        except Exception as exc:
            log.error("execute_instruction error for action=%s: %s", action, exc)
            results.append(f"{action} failed: {exc}")

    return "; ".join(results) if results else "No actions"


def _send_reply(original_subject: str, result_str: str) -> None:
    """Send a confirmation reply to the owner after processing their email instructions."""
    if not OWNER_EMAIL:
        return
    try:
        from services.email_service import EmailService
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        subject = f"Re: {original_subject}" if original_subject else "BizNode — Instructions Processed"
        html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"/>
<style>
  body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f5f7fa;color:#1e293b;}}
  .wrap{{max-width:580px;margin:24px auto;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08);}}
  .hdr{{background:#1e293b;color:#fff;padding:20px 28px;}}
  .hdr h1{{margin:0;font-size:16px;}}
  .body{{padding:24px 28px;}}
  .result-row{{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:10px 14px;margin:8px 0;font-size:13px;color:#166534;}}
  .footer{{background:#f8fafc;border-top:1px solid #e2e8f0;padding:14px 28px;font-size:12px;color:#94a3b8;}}
</style>
</head><body>
<div class="wrap">
  <div class="hdr"><h1>&#10003; BizNode — Instructions Processed</h1></div>
  <div class="body">
    <p style="font-size:14px;color:#475569;margin:0 0 16px">
      Your email instructions have been received and executed at <strong>{now}</strong>.
    </p>
    {"".join(f'<div class="result-row">&#10003; {r.strip()}</div>' for r in result_str.split(";") if r.strip())}
    <p style="font-size:13px;color:#64748b;margin:16px 0 0">
      All actions are logged in your dashboard under <strong>Marketing Hub &rarr; Email Instructions</strong>.
    </p>
  </div>
  <div class="footer">BizNode &nbsp;&middot;&nbsp; Inbox checks every 15 minutes &nbsp;&middot;&nbsp; Reply for more instructions</div>
</div>
</body></html>"""
        svc = EmailService()
        svc._email_type = "instruction_reply"
        svc.send_html_email(to_email=OWNER_EMAIL, subject=subject, html_content=html)
        log.info("Instruction reply sent to %s", OWNER_EMAIL)
    except Exception as exc:
        log.warning("Could not send instruction reply: %s", exc)


# ---------------------------------------------------------------------------
# Orchestrator (called by scheduler)
# ---------------------------------------------------------------------------

def check_email_instructions() -> Dict[str, Any]:
    """
    Full pipeline: fetch unread → parse → execute → log → reply.
    Called every 15 minutes by the scheduler.
    """
    db = _load_db()
    emails = fetch_unread_owner_replies()

    if not emails:
        log.info("IMAP check: no new owner replies")
        return {"processed": 0, "results": []}

    all_results = []
    for em in emails:
        body    = em["body"]
        subject = em.get("subject", "")

        # Check if this is a video task email
        try:
            from videoautomation.email_handler import is_video_task_email, handle_video_task_email
            if is_video_task_email(subject):
                vt_result = handle_video_task_email(em)
                all_results.append({"subject": subject, "result": f"video_task:{vt_result}"})
                log.info("Video task email processed: %s", subject)
                continue
        except ImportError:
            pass

        # Log the received email to email_log
        try:
            db.log_email_received(
                from_addr    = OWNER_EMAIL or "owner",
                subject      = subject,
                body_preview = body[:500],
                email_type   = "owner_instruction",
            )
        except Exception as exc:
            log.warning("email_log received write failed: %s", exc)

        parsed        = parse_instruction(body)
        result_str    = execute_instruction(parsed)
        parsed_action = json.dumps(parsed, ensure_ascii=False)
        status        = "executed"

        db.log_email_instruction(
            raw_email     = body[:2000],
            parsed_action = parsed_action,
            status        = status,
            result        = result_str,
        )
        all_results.append({"subject": subject, "result": result_str})
        log.info("Processed email instruction: %s -> %s", subject, result_str)
        _send_reply(original_subject=subject, result_str=result_str)

    return {"processed": len(emails), "results": all_results}
