import asyncio
import aiosmtplib
from datetime import date, timedelta, datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.database import AsyncSessionLocal
from app.models.contract import Contract
from app.models.recurring_task import RecurringTask
from app.models.task import Task
from app.models.leave_request import LeaveRequest
from app.models.notification import Notification
from app.models.user import User
from app.config import settings
from app.services.recurrence import get_next_due_date
from app.services.notifier import notify_user_by_id, notify_admins, notify_user_group, notify_admin_group


async def send_expiry_email(to_email: str, username: str, contract_title: str,
                             days_left: int, expires_on: date, contract_id: int):
    """Send contract expiry warning email via local Postfix."""
    try:
        msg = MIMEMultipart("alternative")
        msg["Subject"] = f"[Jarvis] Contract expiring in {days_left} days: {contract_title}"
        msg["From"] = settings.SMTP_FROM
        msg["To"] = to_email

        html = f"""
        <p>Hi {username},</p>
        <p>This is a reminder that the following contract is expiring soon:</p>
        <table style="border-collapse:collapse;margin:12px 0">
          <tr><td style="padding:4px 12px 4px 0;color:#555">Contract</td>
              <td style="padding:4px 0"><strong>{contract_title}</strong></td></tr>
          <tr><td style="padding:4px 12px 4px 0;color:#555">Expires on</td>
              <td style="padding:4px 0"><strong>{expires_on}</strong></td></tr>
          <tr><td style="padding:4px 12px 4px 0;color:#555">Days left</td>
              <td style="padding:4px 0"><strong style="color:#e63946">{days_left} days</strong></td></tr>
        </table>
        <p>
          <a href="https://jarvis.goteku.com/contracts/{contract_id}"
             style="background:#206bc4;color:#fff;padding:8px 18px;border-radius:4px;text-decoration:none">
            View Contract
          </a>
        </p>
        <p style="color:#9ca3af;font-size:12px;margin-top:24px">
          — Jarvis · no-reply@jarvis.goteku.com
        </p>
        """

        msg.attach(MIMEText(html, "html"))

        # Connect to local Postfix — no auth, no TLS needed
        await aiosmtplib.send(
            msg,
            hostname=settings.SMTP_HOST,
            port=settings.SMTP_PORT,
            use_tls=False,
            start_tls=False,
        )
    except Exception as e:
        print(f"[scheduler] email error to {to_email}: {e}")


async def check_contract_expiry():
    """Check contracts expiring in ~90 days and send in-app + email notifications."""
    today = date.today()
    notify_from = today + timedelta(days=85)
    notify_to   = today + timedelta(days=95)

    async with AsyncSessionLocal() as db:
        try:
            result = await db.execute(
                select(Contract).where(
                    Contract.period_end >= notify_from,
                    Contract.period_end <= notify_to,
                )
            )
            contracts = result.scalars().all()

            for contract in contracts:
                days_left = (contract.period_end - today).days

                # Find users to notify: created_by + all admins
                user_ids = set()
                if contract.created_by:
                    user_ids.add(contract.created_by)

                admins = await db.execute(select(User).where(User.role == "admin", User.is_active == True))
                for admin in admins.scalars().all():
                    user_ids.add(admin.id)

                # Load all relevant users in one query
                users_result = await db.execute(select(User).where(User.id.in_(user_ids)))
                users = {u.id: u for u in users_result.scalars().all()}

                for user_id in user_ids:
                    # Check if in-app notification already sent (avoid duplicates)
                    existing = await db.scalar(
                        select(Notification).where(
                            Notification.user_id == user_id,
                            Notification.link == f"/contracts/{contract.id}",
                            Notification.title.like("Contract expiring%"),
                        )
                    )
                    if existing:
                        continue

                    # In-app notification
                    db.add(Notification(
                        user_id=user_id,
                        title=f"Kontrak hampir berakhir: {contract.title}",
                        body=f"Berakhir dalam {days_left} hari ({contract.period_end})",
                        link=f"/contracts/{contract.id}",
                    ))

                    # Email notification
                    user = users.get(user_id)
                    if user and user.email:
                        await send_expiry_email(
                            to_email=user.email,
                            username=user.username,
                            contract_title=contract.title,
                            days_left=days_left,
                            expires_on=contract.period_end,
                            contract_id=contract.id,
                        )

            await db.commit()
        except Exception as e:
            await db.rollback()
            print(f"[scheduler] expiry check error: {e}")


async def generate_recurring_tasks():
    """Check all active recurring tasks and create task instances when due."""
    today = date.today()
    async with AsyncSessionLocal() as db:
        try:
            result = await db.execute(
                select(RecurringTask).where(
                    RecurringTask.is_active == True,  # noqa: E712
                    RecurringTask.trigger_mode.in_(["on_schedule", "both"]),
                )
            )
            recurring_list = result.scalars().all()

            for rt in recurring_list:
                try:
                    after = rt.last_generated_due or (today - timedelta(days=1))
                    next_due = get_next_due_date(rt.recurrence_type, rt.recurrence_config, after)
                    trigger_date = next_due - timedelta(days=rt.advance_days)

                    if today >= trigger_date:
                        # Avoid duplicate: check if task already exists for this due date
                        existing = await db.scalar(
                            select(Task).where(
                                Task.recurring_task_id == rt.id,
                                Task.due_date == next_due,
                            )
                        )
                        if not existing:
                            new_task = Task(
                                title=rt.title,
                                assigned_to=rt.assigned_to,
                                created_by=rt.created_by,
                                due_date=next_due,
                                recurring_task_id=rt.id,
                            )
                            db.add(new_task)
                            await db.flush()
                            rt.last_generated_due = next_due
                            print(f"[scheduler] generated recurring task '{rt.title}' due {next_due}")
                            # Notify assignee (in-app)
                            if rt.assigned_to:
                                await notify_user_by_id(
                                    db, rt.assigned_to,
                                    f"Task recurring baru: {rt.title}",
                                    f"Due date: {next_due}",
                                    f"/tasks/{new_task.id}",
                                )
                            # Notify user group (Telegram)
                            await notify_user_group(
                                db,
                                f"Task recurring baru: {rt.title}",
                                f"Due date: {next_due}",
                                f"/tasks/{new_task.id}",
                                thread_key="telegram_thread_task",
                            )
                except Exception as e:
                    print(f"[scheduler] recurring task {rt.id} error: {e}")

            await db.commit()
        except Exception as e:
            await db.rollback()
            print(f"[scheduler] generate_recurring_tasks error: {e}")


async def check_overdue_tasks():
    """Notify assignees about tasks that became overdue today."""
    today = date.today()
    async with AsyncSessionLocal() as db:
        try:
            result = await db.execute(
                select(Task).where(
                    Task.due_date == today - timedelta(days=1),
                    Task.status != "done",
                    Task.assigned_to != None,  # noqa: E711
                )
            )
            tasks = result.scalars().all()
            for task in tasks:
                await notify_user_by_id(
                    db, task.assigned_to,
                    f"Task overdue: {task.title}",
                    f"Task ini melewati due date ({task.due_date}).",
                    f"/tasks/{task.id}",
                )
                await notify_user_group(
                    db,
                    f"Task overdue: {task.title}",
                    f"Due date {task.due_date} terlewat.",
                    f"/tasks/{task.id}",
                    thread_key="telegram_thread_task",
                )
            await db.commit()
        except Exception as e:
            await db.rollback()
            print(f"[scheduler] overdue tasks error: {e}")


async def check_due_tomorrow():
    """Notify assignees about tasks due tomorrow (H-1 reminder)."""
    tomorrow = date.today() + timedelta(days=1)
    async with AsyncSessionLocal() as db:
        try:
            result = await db.execute(
                select(Task).where(
                    Task.due_date == tomorrow,
                    Task.status != "done",
                    Task.assigned_to != None,  # noqa: E711
                )
            )
            tasks = result.scalars().all()
            for task in tasks:
                await notify_user_by_id(
                    db, task.assigned_to,
                    f"Task due besok: {task.title}",
                    f"Due date: {tomorrow}",
                    f"/tasks/{task.id}",
                )
                await notify_user_group(
                    db,
                    f"Task due besok: {task.title}",
                    f"Due date: {tomorrow}",
                    f"/tasks/{task.id}",
                    thread_key="telegram_thread_task",
                )
            await db.commit()
        except Exception as e:
            await db.rollback()
            print(f"[scheduler] due tomorrow error: {e}")


async def check_contracts_expired_today():
    """Notify creator + admins when a contract expires today."""
    today = date.today()
    async with AsyncSessionLocal() as db:
        try:
            result = await db.execute(
                select(Contract).where(Contract.period_end == today - timedelta(days=1))
            )
            contracts = result.scalars().all()
            for contract in contracts:
                title = f"Kontrak telah berakhir: {contract.title}"
                body  = f"Kontrak berakhir pada {contract.period_end}."
                link  = f"/contracts/{contract.id}"
                # In-app: creator + all admins
                if contract.created_by:
                    await notify_user_by_id(db, contract.created_by, title, body, link)
                await notify_admins(db, title, body, link)
                # Telegram: contract topic only
                await notify_user_group(db, title, body, link, thread_key="telegram_thread_contract")
            await db.commit()
        except Exception as e:
            await db.rollback()
            print(f"[scheduler] contracts expired error: {e}")


async def send_daily_digest():
    """Send daily digest HTML email to all admins."""
    today = date.today()
    async with AsyncSessionLocal() as db:
        try:
            # --- Counts ---
            active_count = await db.scalar(
                select(func.count()).select_from(Task).where(
                    Task.status.in_(["todo", "in_progress"]),
                )
            ) or 0
            pending_leave_count = await db.scalar(
                select(func.count()).select_from(LeaveRequest).where(
                    LeaveRequest.status == "pending",
                )
            ) or 0
            expiring_count = await db.scalar(
                select(func.count()).select_from(Contract).where(
                    Contract.period_end >= today,
                    Contract.period_end <= today + timedelta(days=30),
                )
            ) or 0

            # --- Detail lists ---
            overdue_res = await db.execute(
                select(Task).where(
                    Task.due_date < today,
                    Task.status != "done",
                ).order_by(Task.due_date).limit(10)
            )
            overdue_tasks = overdue_res.scalars().all()
            overdue_count = len(overdue_tasks)

            leave_res = await db.execute(
                select(LeaveRequest).where(
                    LeaveRequest.status == "pending",
                ).order_by(LeaveRequest.created_at).limit(10)
            )
            leave_list = leave_res.scalars().all()

            contracts_res = await db.execute(
                select(Contract).where(
                    Contract.period_end >= today,
                    Contract.period_end <= today + timedelta(days=30),
                ).order_by(Contract.period_end).limit(10)
            )
            contracts_list = contracts_res.scalars().all()

            # --- Enrich usernames ---
            user_ids = set()
            for t in overdue_tasks:
                if t.assigned_to:
                    user_ids.add(t.assigned_to)
            for lr in leave_list:
                user_ids.add(lr.user_id)
            users_map: dict = {}
            if user_ids:
                u_res = await db.execute(select(User).where(User.id.in_(user_ids)))
                users_map = {u.id: u.username for u in u_res.scalars().all()}

            # --- Row builders ---
            def _overdue_rows() -> str:
                rows = ""
                for i, t in enumerate(overdue_tasks):
                    bg = "background:#fafafa;" if i % 2 else ""
                    assignee = users_map.get(t.assigned_to, "-") if t.assigned_to else "-"
                    rows += (
                        f'<tr style="{bg}">'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6">{t.title}</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;color:#555">{assignee}</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;color:#dc2626;font-weight:600">{t.due_date}</td>'
                        f'</tr>'
                    )
                return rows

            def _leave_rows() -> str:
                rows = ""
                for i, lr in enumerate(leave_list):
                    bg = "background:#fafafa;" if i % 2 else ""
                    uname = users_map.get(lr.user_id, "-")
                    days = (lr.end_date - lr.start_date).days + 1
                    rows += (
                        f'<tr style="{bg}">'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;font-weight:500">{uname}</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;color:#555">'
                        f'{lr.start_date} → {lr.end_date} &nbsp;'
                        f'<span style="background:#fef3c7;color:#92400e;padding:1px 6px;border-radius:3px;font-size:11px">{days} hari</span>'
                        f'</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;color:#555">{lr.reason or "-"}</td>'
                        f'</tr>'
                    )
                return rows

            def _contract_rows() -> str:
                rows = ""
                for i, c in enumerate(contracts_list):
                    bg = "background:#fafafa;" if i % 2 else ""
                    days_left = (c.period_end - today).days
                    rows += (
                        f'<tr style="{bg}">'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;font-weight:500">{c.title}</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6;color:#555">{c.period_end}</td>'
                        f'<td style="padding:8px 10px;border-bottom:1px solid #f3f4f6">'
                        f'<span style="background:#fef2f2;color:#dc2626;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:700">{days_left} hari</span>'
                        f'</td>'
                        f'</tr>'
                    )
                return rows

            th = ('<tr style="background:#f9fafb">'
                  '<th style="text-align:left;padding:8px 10px;font-size:11px;color:#666;font-weight:600;border-bottom:1px solid #e5e7eb;letter-spacing:.3px">{h1}</th>'
                  '<th style="text-align:left;padding:8px 10px;font-size:11px;color:#666;font-weight:600;border-bottom:1px solid #e5e7eb;letter-spacing:.3px">{h2}</th>'
                  '<th style="text-align:left;padding:8px 10px;font-size:11px;color:#666;font-weight:600;border-bottom:1px solid #e5e7eb;letter-spacing:.3px">{h3}</th>'
                  '</tr>')

            overdue_section = ""
            if overdue_tasks:
                overdue_section = (
                    f'<tr><td style="padding:24px 32px 0">'
                    f'<div style="font-size:13px;font-weight:700;color:#dc2626;margin-bottom:10px;border-left:3px solid #dc2626;padding-left:10px">🔴 Task Overdue ({overdue_count})</div>'
                    f'<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px">'
                    f'{th.format(h1="TASK", h2="ASSIGNEE", h3="DUE DATE")}{_overdue_rows()}'
                    f'</table></td></tr>'
                )

            leave_section = ""
            if leave_list:
                leave_section = (
                    f'<tr><td style="padding:20px 32px 0">'
                    f'<div style="font-size:13px;font-weight:700;color:#d97706;margin-bottom:10px;border-left:3px solid #d97706;padding-left:10px">🏖 Cuti Pending ({pending_leave_count})</div>'
                    f'<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px">'
                    f'{th.format(h1="EMPLOYEE", h2="PERIODE", h3="ALASAN")}{_leave_rows()}'
                    f'</table></td></tr>'
                )

            contracts_section = ""
            if contracts_list:
                contracts_section = (
                    f'<tr><td style="padding:20px 32px 0">'
                    f'<div style="font-size:13px;font-weight:700;color:#16a34a;margin-bottom:10px;border-left:3px solid #16a34a;padding-left:10px">⚠️ Kontrak Berakhir ≤30 Hari ({expiring_count})</div>'
                    f'<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px">'
                    f'{th.format(h1="KONTRAK", h2="BERAKHIR", h3="SISA")}{_contract_rows()}'
                    f'</table></td></tr>'
                )

            html = f"""<!DOCTYPE html>
<html lang="id"><head><meta charset="utf-8"></head>
<body style="margin:0;padding:32px 16px;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.10);overflow:hidden">
  <tr><td style="background:#1D9E75;padding:26px 32px">
    <table width="100%" cellpadding="0" cellspacing="0"><tr>
      <td><div style="color:#fff;font-size:22px;font-weight:700;letter-spacing:-0.3px">📊 Daily Digest</div>
          <div style="color:rgba(255,255,255,.75);font-size:13px;margin-top:5px">{today} &nbsp;·&nbsp; Jarvis Internal</div></td>
      <td align="right"><div style="background:rgba(255,255,255,.15);border-radius:6px;padding:8px 14px;color:#fff;font-size:12px;font-weight:600">08:00 WIB</div></td>
    </tr></table>
  </td></tr>
  <tr><td style="padding:24px 32px 0">
    <table width="100%" cellpadding="0" cellspacing="0"><tr>
      <td style="width:22%;background:#fff1f0;border-radius:8px;padding:16px 10px;text-align:center;border:1px solid #fecaca">
        <div style="font-size:32px;font-weight:800;color:#dc2626;line-height:1">{overdue_count}</div>
        <div style="font-size:10px;color:#888;margin-top:5px;font-weight:600;letter-spacing:.5px">OVERDUE</div>
      </td>
      <td style="width:4%"></td>
      <td style="width:22%;background:#eff6ff;border-radius:8px;padding:16px 10px;text-align:center;border:1px solid #bfdbfe">
        <div style="font-size:32px;font-weight:800;color:#2563eb;line-height:1">{active_count}</div>
        <div style="font-size:10px;color:#888;margin-top:5px;font-weight:600;letter-spacing:.5px">TASK AKTIF</div>
      </td>
      <td style="width:4%"></td>
      <td style="width:22%;background:#fefce8;border-radius:8px;padding:16px 10px;text-align:center;border:1px solid #fde68a">
        <div style="font-size:32px;font-weight:800;color:#d97706;line-height:1">{pending_leave_count}</div>
        <div style="font-size:10px;color:#888;margin-top:5px;font-weight:600;letter-spacing:.5px">CUTI PENDING</div>
      </td>
      <td style="width:4%"></td>
      <td style="width:22%;background:#f0fdf4;border-radius:8px;padding:16px 10px;text-align:center;border:1px solid #bbf7d0">
        <div style="font-size:32px;font-weight:800;color:#16a34a;line-height:1">{expiring_count}</div>
        <div style="font-size:10px;color:#888;margin-top:5px;font-weight:600;letter-spacing:.5px">KONTRAK ≤30H</div>
      </td>
    </tr></table>
  </td></tr>
  {overdue_section}
  {leave_section}
  {contracts_section}
  <tr><td style="padding:28px 32px 32px;text-align:center;border-top:1px solid #f0f0f0">
    <div style="margin-top:20px">
      <a href="https://jarvis.goteku.com"
         style="display:inline-block;background:#1D9E75;color:#fff;padding:12px 32px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:600;letter-spacing:.2px">
        Buka Jarvis &rarr;
      </a>
    </div>
    <div style="color:#9ca3af;font-size:11px;margin-top:16px;line-height:1.6">
      Anda menerima email ini karena Anda adalah admin Jarvis.<br>
      no-reply@jarvis.goteku.com
    </div>
  </td></tr>
</table>
</td></tr></table>
</body></html>"""

            # --- Send to all admins with email ---
            admins_res = await db.execute(
                select(User).where(User.role == "admin", User.is_active == True)  # noqa: E712
            )
            for admin in admins_res.scalars().all():
                if not admin.email:
                    continue
                try:
                    email_msg = MIMEMultipart("alternative")
                    email_msg["Subject"] = f"[Jarvis] Daily Digest — {today}"
                    email_msg["From"] = settings.SMTP_FROM
                    email_msg["To"] = admin.email
                    email_msg.attach(MIMEText(html, "html"))
                    await aiosmtplib.send(
                        email_msg,
                        hostname=settings.SMTP_HOST,
                        port=settings.SMTP_PORT,
                        use_tls=False,
                        start_tls=False,
                    )
                    print(f"[scheduler] daily digest sent to {admin.email}")
                except Exception as e:
                    print(f"[scheduler] digest email error to {admin.email}: {e}")

        except Exception as e:
            print(f"[scheduler] daily digest error: {e}")


async def _already_ran_today(key: str) -> bool:
    """Return True if this check already ran today (dedup via app_settings)."""
    from app.models.app_setting import AppSetting
    today_str = date.today().isoformat()
    async with AsyncSessionLocal() as db:
        setting = await db.get(AppSetting, key)
        return setting is not None and setting.value == today_str


async def _already_ran_within_days(key: str, days: int) -> bool:
    """Return True if this check already ran within the last `days` days."""
    from app.models.app_setting import AppSetting
    today = date.today()
    async with AsyncSessionLocal() as db:
        setting = await db.get(AppSetting, key)
        if not setting or not setting.value:
            return False
        try:
            last_run = date.fromisoformat(setting.value)
            return (today - last_run).days < days
        except ValueError:
            return False


async def _mark_ran_today(key: str) -> None:
    """Record that this check ran today."""
    from app.models.app_setting import AppSetting
    today_str = date.today().isoformat()
    async with AsyncSessionLocal() as db:
        setting = await db.get(AppSetting, key)
        if setting:
            setting.value = today_str
        else:
            db.add(AppSetting(key=key, value=today_str))
        await db.commit()


async def _seconds_until_8am_wib() -> float:
    """Return seconds to wait until next 08:00 WIB (UTC+7)."""
    now_wib = datetime.utcnow() + timedelta(hours=7)
    target = now_wib.replace(hour=8, minute=0, second=0, microsecond=0)
    if now_wib >= target:
        target += timedelta(days=1)
    return (target - now_wib).total_seconds()


async def run_scheduler():
    """Run daily checks at 08:00 WIB. Dedup prevents double-run on restart."""
    while True:
        wait = await _seconds_until_8am_wib()
        print(f"[scheduler] sleeping {wait/3600:.1f}h until 08:00 WIB")
        await asyncio.sleep(wait)

        # Each check is guarded by a dedup key — safe to restart anytime
        if not await _already_ran_within_days("sched_contract_expiry", 3):
            try:
                await check_contract_expiry()
                await _mark_ran_today("sched_contract_expiry")
            except Exception as e:
                print(f"[scheduler] contract expiry error: {e}")

        if not await _already_ran_today("sched_recurring"):
            try:
                await generate_recurring_tasks()
                await _mark_ran_today("sched_recurring")
            except Exception as e:
                print(f"[scheduler] recurring tasks error: {e}")

        if not await _already_ran_today("sched_overdue"):
            try:
                await check_overdue_tasks()
                await _mark_ran_today("sched_overdue")
            except Exception as e:
                print(f"[scheduler] overdue tasks error: {e}")

        if not await _already_ran_today("sched_due_tomorrow"):
            try:
                await check_due_tomorrow()
                await _mark_ran_today("sched_due_tomorrow")
            except Exception as e:
                print(f"[scheduler] due tomorrow error: {e}")

        if not await _already_ran_today("sched_contracts_expired"):
            try:
                await check_contracts_expired_today()
                await _mark_ran_today("sched_contracts_expired")
            except Exception as e:
                print(f"[scheduler] contracts expired error: {e}")

        if not await _already_ran_today("sched_daily_digest"):
            try:
                await send_daily_digest()
                await _mark_ran_today("sched_daily_digest")
            except Exception as e:
                print(f"[scheduler] daily digest error: {e}")
