"""P1 static auto-response service.

Sends a single, static (non-AI) auto-response on inbound threads when:
  1. The org's plan includes the 'autoresponse_static' feature, AND
  2. The org is currently "outside working hours" (away mode OR no member
     is scheduled to be online right now), AND
  3. The org has an active AutoresponseTemplate configured.

Called as a post-sync hook (see send_pending_autoresponses) so it never runs
inline with the sync transaction itself. Any failure on a single thread must
not abort the batch — all DB/provider access is defensively wrapped.
"""
import json
import logging
import datetime as dt

import bleach

logger = logging.getLogger(__name__)


def _org_local_now(org, now_utc: dt.datetime) -> dt.datetime:
    """Convert now_utc to the org's configured timezone.

    Falls back to UTC on invalid/missing tz names and logs a warning.
    """
    from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
    tz_name = getattr(org, "timezone", None) or "UTC"
    try:
        tz = ZoneInfo(tz_name)
    except (ZoneInfoNotFoundError, KeyError):
        logger.warning(
            "_org_local_now: unknown timezone %r for org %s, falling back to UTC",
            tz_name, getattr(org, "id", "?"),
        )
        tz = ZoneInfo("UTC")
    # now_utc may be naive (utcnow()) — treat it as UTC explicitly.
    if now_utc.tzinfo is None:
        now_utc = now_utc.replace(tzinfo=ZoneInfo("UTC"))
    return now_utc.astimezone(tz)


def org_has_feature(org, feature: str) -> bool:
    """Thin wrapper around plan_service.check_feature, named for clarity in service code."""
    from .plan_service import check_feature
    return check_feature(org, feature)


def is_outside_schedule(org, now_utc, public_session=None) -> bool:
    """True if the org is 'outside working hours' right now.

    MemberSchedule times are interpreted as ORG-LOCAL time (using
    org.timezone, an IANA tz name).  now_utc is converted to the org's
    local time before comparing weekday and time-of-day.

    Fail-safe: any error while consulting the DB returns False (never
    auto-respond when we're not sure the org is actually away).
    """
    try:
        # 1. Away mode takes priority.
        if getattr(org, "away_mode_enabled", False):
            away_until = getattr(org, "away_until_date", None)
            if away_until is None or now_utc.date() <= away_until:
                return True
            # away_until_date has passed — best-effort auto-clear.
            if public_session is not None:
                try:
                    org.away_mode_enabled = False
                except Exception:
                    logger.debug("is_outside_schedule: could not auto-clear away_mode_enabled", exc_info=True)
            # fall through to schedule check

        from ..public_models import User, MemberSchedule

        _own_session = public_session is None
        if _own_session:
            from ..db import SessionLocal
            from sqlalchemy import text
            public_session = SessionLocal()
            public_session.execute(text("SET search_path TO public"))

        try:
            user_ids = [
                row[0]
                for row in public_session.query(User.id).filter(User.org_id == org.id).all()
            ]
            if not user_ids:
                return False

            schedules = (
                public_session.query(MemberSchedule)
                .filter(MemberSchedule.user_id.in_(user_ids))
                .all()
            )
            if not schedules:
                # No schedule configured = never auto-respond; explicit opt-in by design.
                return False

            # Convert to org-local time for weekday + time comparison.
            local_now = _org_local_now(org, now_utc)
            weekday = local_now.weekday()        # Monday=0 .. Sunday=6
            current_time = local_now.time().replace(tzinfo=None)
            for sched in schedules:
                if sched.weekday != weekday:
                    continue
                if sched.start_time <= current_time <= sched.end_time:
                    return False  # covered by at least one member right now
            return True
        finally:
            if _own_session:
                public_session.close()
    except Exception:
        logger.warning("is_outside_schedule: failed, defaulting to False (no auto-response)", exc_info=True)
        return False


def get_active_template(session):
    """Return the active AutoresponseTemplate for the current tenant (is_active=True), or None.

    `session` is an active tenant-schema session.
    """
    from ..models import AutoresponseTemplate
    return session.query(AutoresponseTemplate).filter_by(is_active=True).first()


def resolve_body(template, lang: str | None) -> str:
    """Return the best body_html for *template* given the customer's language tag.

    Matching rules (applied in order):
      1. If lang is None or empty → use template.body_html (default).
      2. Case-insensitive exact match on translation.lang.
      3. Primary-subtag match: 'pt-BR' request matches 'pt' translation (and
         vice-versa — both directions truncated to primary subtag).
      4. No match → template.body_html (default).

    `template.translations` must be loaded (lazy or eager) before calling.
    """
    if not lang:
        return template.body_html

    translations = getattr(template, "translations", None) or []
    if not translations:
        return template.body_html

    lang_lower = lang.lower()
    lang_primary = lang_lower.split("-")[0]  # 'pt-BR' → 'pt'

    # Build lookup: lower(translation.lang) → translation
    by_full: dict[str, str] = {}
    by_primary: dict[str, str] = {}
    for tr in translations:
        tl = tr.lang.lower()
        tp = tl.split("-")[0]
        by_full[tl] = tr.body_html
        # primary-subtag bucket: first entry wins when multiple match (e.g. 'pt' and 'pt-PT')
        by_primary.setdefault(tp, tr.body_html)

    # 1. Exact match
    if lang_lower in by_full:
        return by_full[lang_lower]
    # 2. Primary-subtag match (covers stored 'pt' vs request 'pt-BR' and vice versa)
    if lang_primary in by_primary:
        return by_primary[lang_primary]

    return template.body_html


def send_autoresponse(
    provider, session, *, thread_provider_id, ticket_id, template, now_utc,
    customer_language: str | None = None,
) -> bool:
    """Send ONE static auto-response on a thread and record it locally.

    customer_language: BCP-47 tag from the thread (may be None). Used to
    select the best translation via resolve_body().

    Returns True on successful provider send + local insert. Never raises —
    one bad thread must not abort the batch.
    """
    try:
        body_html = resolve_body(template, customer_language)
        plain = bleach.clean(body_html, tags=[], strip=True).strip()
        if not plain:
            logger.warning("send_autoresponse: template body is empty after stripping HTML, skipping")
            return False

        ok = provider.send_reply(thread_provider_id, plain)
        if not ok:
            logger.warning("send_autoresponse: provider.send_reply failed for thread %s", thread_provider_id)
            return False

        from ..models import Tickets, AddonsMessages, AuditLog

        ticket = session.query(Tickets).get(ticket_id)
        if ticket is None:
            logger.warning("send_autoresponse: ticket %s not found", ticket_id)
            return False

        session.add(
            AddonsMessages(
                thread=ticket.thread,
                provider_message_id=None,
                direction="outbound",
                author_display="Support Agent",
                body_text=plain,
                body_raw=body_html,
                created_at=now_utc,
                is_auto_response=True,
            )
        )

        if ticket.first_response_at is None:
            ticket.first_response_at = now_utc
        # Do NOT set status = "answered" — auto-responses leave the ticket in
        # its current state (usually "open") so it remains in the agent's queue.
        ticket.is_read = False  # keep it surfaced as unread

        session.add(
            AuditLog(
                action="autoresponse_sent",
                payload_json=json.dumps({"ticket_id": ticket_id, "thread_id": thread_provider_id}),
            )
        )
        return True
    except Exception:
        logger.warning("send_autoresponse: failed for ticket %s / thread %s", ticket_id, thread_provider_id, exc_info=True)
        return False


def send_pending_autoresponses(provider, pending) -> int:
    """Post-sync hook. `pending` is a list of dicts with keys:
       ticket_id, thread_id (db), thread_provider_id, latest_inbound_id.
    Returns count of auto-responses actually sent.
    """
    if not pending:
        return 0

    now_utc = dt.datetime.utcnow()

    from contextlib import suppress
    _schema = None
    with suppress(RuntimeError, ImportError):
        from flask import g as _g
        _schema = getattr(_g, "tenant_schema", None)
    if _schema is None:
        from ..db import _worker_tenant_schema as _wts
        _schema = _wts
    if not _schema:
        logger.debug("send_pending_autoresponses: could not resolve tenant schema, skipping")
        return 0

    # NOTE: the provider is resolved further down, from the org this schema
    # belongs to — NOT here. Building ProviderAddons() with no key made every
    # tenant's auto-response go out through the OWNER's Addons account.
    from ..db import SessionLocal
    from ..public_models import Organization
    from sqlalchemy import text

    public_session = SessionLocal()
    try:
        public_session.execute(text("SET search_path TO public"))
        org = public_session.query(Organization).filter_by(schema_name=_schema).one_or_none()
        if org is None or not org_has_feature(org, "autoresponse_static"):
            return 0

        if not is_outside_schedule(org, now_utc, public_session=public_session):
            return 0

        # Bind to THIS org's Addons account before sending anything outbound.
        if provider is None:
            from .provider_factory import provider_for_org, MissingAddonsKey
            try:
                provider = provider_for_org(org)
            except MissingAddonsKey:
                logger.warning(
                    "send_pending_autoresponses: org %s has no Addons key; "
                    "skipping rather than sending from the owner's account", org.id)
                return 0

        # Persist any best-effort away-mode auto-clear performed above.
        try:
            public_session.commit()
        except Exception:
            logger.debug("send_pending_autoresponses: public session commit failed", exc_info=True)
            public_session.rollback()
    finally:
        public_session.close()

    from ..db import session_scope

    count = 0
    with session_scope() as session:
        template = get_active_template(session)
        if template is None:
            return 0

        from ..models import Tickets

        sent_thread_ids = set()
        for item in pending:
            thread_provider_id = item["thread_provider_id"]
            if thread_provider_id in sent_thread_ids:
                continue  # double-fire guard within one run

            # Resolve customer_language from the thread for translation selection.
            customer_language: str | None = None
            try:
                ticket = session.query(Tickets).get(item["ticket_id"])
                if ticket and ticket.thread:
                    customer_language = ticket.thread.customer_language
            except Exception:
                logger.debug(
                    "send_pending_autoresponses: could not resolve customer_language for ticket %s",
                    item["ticket_id"], exc_info=True,
                )

            if send_autoresponse(
                provider,
                session,
                thread_provider_id=thread_provider_id,
                ticket_id=item["ticket_id"],
                template=template,
                now_utc=now_utc,
                customer_language=customer_language,
            ):
                sent_thread_ids.add(thread_provider_id)
                count += 1

    logger.info("send_pending_autoresponses: sent %d auto-response(s)", count)
    return count
