"""منطق بخش «کارها» (تسک‌بورد شبیه تیک‌تیک): پوشه‌ها، تسک‌ها، نماهای آماده، تکرار و افزودن گروهی.

قراردادها:
- due_date روز تقویم ایرانه و مستقیم با iran_today() مقایسه می‌شه (هیچ تبدیل UTC‌ای لازم نیست).
- done_at مثل بقیه‌ی ستون‌های زمانی، UTC ذخیره می‌شه.
- «انجام‌شده بودن پوشه» هیچ‌جا ذخیره نمی‌شه و همیشه از روی تسک‌هاش حساب می‌شه
  (پوشه وقتی تمام‌شده‌ست که حداقل یک تسک داشته باشه و هیچ تسک بازی نداشته باشه).
"""
import json
from datetime import time, timedelta

import jdatetime
from sqlalchemy import case, func, or_, update
from sqlalchemy.orm import selectinload

from app.extensions import db
from app.models.customer import Customer
from app.models.task_board import TaskFolder, TaskItem, TaskSubitem
from app.services import customer_service
from app.services.jalali import iran_today, jalali_str_to_gregorian_date, to_jalali_str, utc_now

DEFAULT_FOLDER_NAME = "پوشه اصلی"
MAX_SUBTASKS = 50

# (مقدار، عنوان، کلید رنگ) — رنگ‌ها از توکن‌های variables.css خونده می‌شن، نه کد رنگ ثابت
PRIORITIES = [
    (3, "اولویت بالا", "danger"),
    (2, "اولویت متوسط", "warning"),
    (1, "اولویت پایین", "info"),
    (0, "بدون اولویت", "none"),
]
PRIORITY_LABELS = {value: label for value, label, _color in PRIORITIES}

REPEAT_CHOICES = [
    ("", "بدون تکرار"),
    ("daily", "روزانه"),
    ("weekly", "هفتگی"),
    ("monthly", "ماهانه"),
    ("quarterly", "هر ۳ ماه"),
    ("yearly", "سالانه"),
]
REPEAT_LABELS = {key: label for key, label in REPEAT_CHOICES if key}

FOLDER_COLORS = [
    ("gold", "طلایی"),
    ("info", "آبی"),
    ("success", "سبز"),
    ("warning", "نارنجی"),
    ("danger", "قرمز"),
    ("purple", "بنفش"),
    ("pink", "صورتی"),
]
FOLDER_COLOR_KEYS = {key for key, _label in FOLDER_COLORS}

VIEWS = [
    {"key": "today", "title": "امروز", "color": "gold", "hint": "سررسید امروز و عقب‌افتاده‌ها"},
    {"key": "tomorrow", "title": "فردا", "color": "info", "hint": "سررسید فردا"},
    {"key": "week", "title": "۷ روز آینده", "color": "purple", "hint": "تا یک هفته‌ی آینده"},
    {"key": "later", "title": "بعداً", "color": "warning", "hint": "بعد از یک هفته"},
    {"key": "nodate", "title": "بدون تاریخ", "color": "pink", "hint": "هنوز تاریخ نخورده"},
    {"key": "done", "title": "انجام‌شده", "color": "success", "hint": "تیک‌خورده‌ها"},
]
VIEW_MAP = {view["key"]: view for view in VIEWS}


# ---------------------------------------------------------------------------
# پوشه‌ها
# ---------------------------------------------------------------------------

def ensure_default_folder(user_id=None) -> TaskFolder:
    """اولین بار که وارد بخش کارها می‌شیم، یک «پوشه اصلی» ساخته می‌شه تا صفحه خالی و بلااستفاده نباشه."""
    folder = TaskFolder.query.filter_by(name=DEFAULT_FOLDER_NAME).first()
    if folder is None:
        folder = TaskFolder(name=DEFAULT_FOLDER_NAME, color="gold", created_by=user_id)
        db.session.add(folder)
        db.session.commit()
    return folder


def folder_rows(include_archived=False):
    """پوشه‌ها همراه با پیشرفتشون — با یک کوئری گروهی، نه یک کوئری به ازای هر پوشه."""
    stats = (
        db.session.query(
            TaskItem.folder_id.label("folder_id"),
            func.count(TaskItem.id).label("total"),
            func.sum(case((TaskItem.is_done.is_(True), 1), else_=0)).label("done_count"),
            func.max(TaskItem.done_at).label("last_done_at"),
        )
        .group_by(TaskItem.folder_id)
        .subquery()
    )
    query = (
        db.session.query(
            TaskFolder,
            func.coalesce(stats.c.total, 0),
            func.coalesce(stats.c.done_count, 0),
            stats.c.last_done_at,
        )
        .outerjoin(stats, stats.c.folder_id == TaskFolder.id)
    )
    if not include_archived:
        query = query.filter(TaskFolder.is_archived.is_(False))

    rows = []
    for folder, total, done_count, last_done_at in query.order_by(TaskFolder.name, TaskFolder.id).all():
        total = int(total or 0)
        done_count = int(done_count or 0)
        rows.append({
            "folder": folder,
            "total": total,
            "done": done_count,
            "open": total - done_count,
            "is_complete": total > 0 and done_count == total,
            "last_done_at": last_done_at,
        })
    return rows


def folder_progress(folder_id):
    """(تعداد کل، تعداد انجام‌شده، تمام‌شده؟) برای یک پوشه — بعد از تیک زدن هم همین برمی‌گرده."""
    total, done_count = (
        db.session.query(
            func.count(TaskItem.id),
            func.coalesce(func.sum(case((TaskItem.is_done.is_(True), 1), else_=0)), 0),
        )
        .filter(TaskItem.folder_id == folder_id)
        .one()
    )
    total = int(total or 0)
    done_count = int(done_count or 0)
    return {"total": total, "done": done_count, "open": total - done_count,
            "is_complete": total > 0 and done_count == total}


# ---------------------------------------------------------------------------
# نماهای آماده و لیست تسک‌ها
# ---------------------------------------------------------------------------

def view_filters(key):
    today = iran_today()
    if key == "today":
        return [TaskItem.is_done.is_(False), TaskItem.due_date.isnot(None), TaskItem.due_date <= today]
    if key == "tomorrow":
        return [TaskItem.is_done.is_(False), TaskItem.due_date == today + timedelta(days=1)]
    if key == "week":
        return [TaskItem.is_done.is_(False), TaskItem.due_date.isnot(None), TaskItem.due_date <= today + timedelta(days=6)]
    if key == "later":
        return [TaskItem.is_done.is_(False), TaskItem.due_date > today + timedelta(days=6)]
    if key == "nodate":
        return [TaskItem.is_done.is_(False), TaskItem.due_date.is_(None)]
    if key == "done":
        return [TaskItem.is_done.is_(True)]
    return []  # «همه»


def items_query(view=None, folder_id=None, assignee_id=None, search=None, include_done=True):
    query = TaskItem.query.options(
        selectinload(TaskItem.customer),
        selectinload(TaskItem.assignee),
        selectinload(TaskItem.completer),
        selectinload(TaskItem.subtasks),
        selectinload(TaskItem.folder),
    )
    if folder_id:
        query = query.filter(TaskItem.folder_id == folder_id)
    else:
        # نماهای آماده فقط پوشه‌های بایگانی‌نشده رو نشون می‌دن
        query = query.join(TaskFolder, TaskFolder.id == TaskItem.folder_id).filter(TaskFolder.is_archived.is_(False))
    if view:
        query = query.filter(*view_filters(view))
    if not include_done:
        query = query.filter(TaskItem.is_done.is_(False))
    if assignee_id:
        query = query.filter(TaskItem.assignee_id == assignee_id)
    if search:
        matching_customers = db.session.query(Customer.id).filter(customer_service.search_condition(search))
        query = query.filter(or_(
            TaskItem.title.icontains(search, autoescape=True),
            TaskItem.notes.icontains(search, autoescape=True),
            TaskItem.customer_id.in_(matching_customers),
        ))

    if view == "done":
        return query.order_by(TaskItem.done_at.desc(), TaskItem.id.desc())
    # تسک‌های باز اول، بعد بر اساس تاریخ سررسید (بدون‌تاریخ‌ها ته لیست)، بعد اولویت
    return query.order_by(
        TaskItem.is_done.asc(),
        TaskItem.due_date.is_(None),
        TaskItem.due_date.asc(),
        TaskItem.priority.desc(),
        TaskItem.id.desc(),
    )


def list_items(limit=None, **filters):
    """(ردیف‌ها، تعداد کل) — سقف پیش‌فرض لیست‌ها از listing.page_limit() میاد."""
    query = items_query(**filters)
    total = query.order_by(None).count()
    if limit:
        query = query.limit(limit)
    return query.all(), total


def view_counts(assignee_id=None):
    counts = {}
    for view in VIEWS:
        query = (
            db.session.query(func.count(TaskItem.id))
            .join(TaskFolder, TaskFolder.id == TaskItem.folder_id)
            .filter(TaskFolder.is_archived.is_(False))
            .filter(*view_filters(view["key"]))
        )
        if assignee_id:
            query = query.filter(TaskItem.assignee_id == assignee_id)
        counts[view["key"]] = int(query.scalar() or 0)
    return counts


# ---------------------------------------------------------------------------
# خواندن مقادیر فرم
# ---------------------------------------------------------------------------

def parse_due_date(raw):
    """رشته‌ی شمسی -> date؛ خالی یعنی بدون تاریخ. خروجی: (تاریخ، خطا)."""
    raw = (raw or "").strip()
    if not raw:
        return None, None
    try:
        return jalali_str_to_gregorian_date(raw), None
    except Exception:
        return None, "تاریخ وارد شده معتبر نیست"


def parse_due_time(hour_raw, minute_raw):
    hour = (hour_raw or "").strip()
    if hour == "":
        return None
    try:
        return time(int(hour) % 24, int((minute_raw or "0").strip() or 0) % 60)
    except ValueError:
        return None


def parse_priority(raw):
    try:
        value = int(raw)
    except (TypeError, ValueError):
        return 0
    return value if value in PRIORITY_LABELS else 0


def parse_repeat(raw):
    raw = (raw or "").strip()
    return raw if raw in REPEAT_LABELS else None


def parse_color(raw):
    raw = (raw or "").strip()
    return raw if raw in FOLDER_COLOR_KEYS else "gold"


def _int_or_none(raw):
    try:
        value = int(raw)
    except (TypeError, ValueError):
        return None
    return value or None


# ---------------------------------------------------------------------------
# ساخت و ویرایش تسک
# ---------------------------------------------------------------------------

def sync_subtasks(item, raw):
    """زیرتسک‌ها با شناسه به‌روز می‌شن تا ویرایش عنوانِ تسک، تیک‌های قبلی رو پاک نکنه.

    اگر فیلد اصلاً ارسال نشده باشه (raw=None) زیرتسک‌ها دست نمی‌خورن؛ فقط یک لیست خالیِ صریح پاکشون می‌کنه.
    """
    if raw is None:
        return
    try:
        payload = json.loads(raw or "[]")
    except ValueError:
        return
    if not isinstance(payload, list):
        return

    existing = {sub.id: sub for sub in item.subtasks}
    keep = set()
    for row in payload[:MAX_SUBTASKS]:
        if not isinstance(row, dict):
            continue
        title = (row.get("title") or "").strip()[:300]
        if not title:
            continue
        done = bool(row.get("done"))
        sub = existing.get(row.get("id")) if isinstance(row.get("id"), int) else None
        if sub is not None:
            sub.title = title
            sub.is_done = done
            keep.add(sub.id)
        else:
            item.subtasks.append(TaskSubitem(title=title, is_done=done))
    for sub_id, sub in existing.items():
        if sub_id not in keep:
            item.subtasks.remove(sub)  # delete-orphan خودش ردیف رو حذف می‌کنه


def save_item(form, user, item=None):
    """ساخت یا ویرایش تسک از روی فرم. خروجی: (تسک، خطا)."""
    title = (form.get("title") or "").strip()[:300]
    customer_id = _int_or_none(form.get("customer_id"))
    customer = db.session.get(Customer, customer_id) if customer_id else None
    if customer is None:
        customer_id = None

    if not title and customer is not None:
        title = customer.full_name.strip()[:300]  # «مشتری رو بذار توی پوشه» بدون تایپ عنوان
    if not title:
        return None, "عنوان تسک را بنویسید یا یک مشتری انتخاب کنید"

    folder_id = _int_or_none(form.get("folder_id"))
    folder = db.session.get(TaskFolder, folder_id) if folder_id else None
    if folder is None:
        folder = item.folder if item is not None else ensure_default_folder(user.id)

    due_date, error = parse_due_date(form.get("due_date"))
    if error:
        return None, error
    due_time = parse_due_time(form.get("due_hour"), form.get("due_minute")) if due_date else None
    repeat_rule = parse_repeat(form.get("repeat_rule"))
    if repeat_rule and not due_date:
        return None, "برای تسک تکرارشونده باید تاریخ سررسید مشخص باشد"

    if item is None:
        item = TaskItem(created_by=user.id)
        db.session.add(item)

    item.folder = folder
    item.title = title
    item.customer_id = customer_id
    item.assignee_id = _int_or_none(form.get("assignee_id"))
    item.due_date = due_date
    item.due_time = due_time
    item.priority = parse_priority(form.get("priority"))
    item.repeat_rule = repeat_rule
    item.notes = (form.get("notes") or "").strip() or None

    db.session.flush()
    sync_subtasks(item, form.get("subtasks"))
    db.session.commit()
    return item, None


def bulk_add_customers(folder, customer_ids, due_date, assignee_id, user):
    """افزودن گروهی مشتری‌ها به یک پوشه — مشتری‌هایی که تسکِ بازشون از قبل توی همین پوشه هست، رد می‌شن."""
    customer_ids = [cid for cid in {_int_or_none(cid) for cid in customer_ids} if cid]
    if not customer_ids:
        return 0, 0

    customers = (
        Customer.query.filter(Customer.id.in_(customer_ids), Customer.is_deleted.is_(False)).all()
    )
    already = {
        row[0]
        for row in db.session.query(TaskItem.customer_id).filter(
            TaskItem.folder_id == folder.id,
            TaskItem.is_done.is_(False),
            TaskItem.customer_id.in_(customer_ids),
        )
    }
    created = 0
    for customer in customers:
        if customer.id in already:
            continue
        db.session.add(TaskItem(
            folder_id=folder.id,
            title=customer.full_name.strip()[:300] or f"مشتری #{customer.id}",
            customer_id=customer.id,
            assignee_id=assignee_id,
            due_date=due_date,
            created_by=user.id,
        ))
        created += 1
    db.session.commit()
    return created, len(customers) - created


# ---------------------------------------------------------------------------
# تیک زدن و تکرار
# ---------------------------------------------------------------------------

def set_done(item_id, done, user):
    """تیک/برداشتن تیک به‌صورت اتمیک.

    شرط روی خود UPDATE گذاشته شده تا دو بار زدن پشت‌سرهم (یا دو کاربر هم‌زمان) دو بار اجرا نشه —
    مهم است چون تیک زدنِ تسک تکرارشونده یک تسک جدید می‌سازد.
    خروجی: (تسک، تسکِ تکرارِ ساخته‌شده یا None، آیا واقعاً تغییر کرد؟)
    """
    if done:
        guard = TaskItem.is_done.is_(False)
        values = {"is_done": True, "done_at": utc_now(), "done_by": user.id}
    else:
        guard = TaskItem.is_done.is_(True)
        values = {"is_done": False, "done_at": None, "done_by": None}

    changed = db.session.execute(
        update(TaskItem)
        .where(TaskItem.id == item_id, guard)
        .values(**values)
        .execution_options(synchronize_session=False)
    ).rowcount
    db.session.commit()

    item = db.session.get(TaskItem, item_id)
    if item is None:
        return None, None, False

    spawned = None
    if changed and done and item.repeat_rule and item.due_date:
        spawned = _spawn_repeat(item, user)
    return item, spawned, bool(changed)


def _spawn_repeat(item, user):
    """تسک بعدیِ یک تسک تکرارشونده؛ زیرتسک‌ها بدون تیک کپی می‌شن."""
    next_date = next_due_date(item.due_date, item.repeat_rule)
    if next_date is None:
        return None
    copy = TaskItem(
        folder_id=item.folder_id,
        title=item.title,
        customer_id=item.customer_id,
        assignee_id=item.assignee_id,
        due_date=next_date,
        due_time=item.due_time,
        priority=item.priority,
        repeat_rule=item.repeat_rule,
        notes=item.notes,
        created_by=user.id,
    )
    for sub in item.subtasks:
        copy.subtasks.append(TaskSubitem(title=sub.title, is_done=False))
    db.session.add(copy)
    db.session.commit()
    return copy


def next_due_date(due_date, rule, today=None):
    """تاریخ سررسید بعدی — همیشه بعد از امروز.

    اگر تسکِ ماهانه دو ماه عقب افتاده باشه، تاریخ بعدی از «امروز» حساب می‌شه نه از تاریخ قدیمی،
    وگرنه بلافاصله دوباره عقب‌افتاده ساخته می‌شد.
    """
    if due_date is None or rule not in REPEAT_LABELS:
        return None
    today = today or iran_today()
    moment = max(due_date, today)
    for _step in range(60):
        moment = _advance(moment, rule)
        if moment > today:
            return moment
    return moment


def _advance(day, rule):
    if rule == "daily":
        return day + timedelta(days=1)
    if rule == "weekly":
        return day + timedelta(days=7)
    months = {"monthly": 1, "quarterly": 3, "yearly": 12}.get(rule, 1)
    # ماه و سال روی تقویم شمسی جلو می‌رن (اسفند ۲۹/۳۰ روزه هم درست حساب می‌شه)
    jalali = jdatetime.date.fromgregorian(date=day)
    index = jalali.month - 1 + months
    year = jalali.year + index // 12
    month = index % 12 + 1
    max_day = jdatetime.j_days_in_month[month - 1]
    if month == 12 and jdatetime.date(year, 1, 1).isleap():
        max_day = 30
    return jdatetime.date(year, month, min(jalali.day, max_day)).togregorian()


# ---------------------------------------------------------------------------
# خروجی JSON برای تیک زدن (todo.js)
# ---------------------------------------------------------------------------

def item_payload(item):
    return {
        "id": item.id,
        "is_done": item.is_done,
        "done_by": item.completer.full_name if item.completer else None,
        "done_at": to_jalali_str(item.done_at, with_time=True) if item.done_at else None,
        "folder_id": item.folder_id,
        "customer_id": item.customer_id,
    }
