# -*- coding: utf-8 -*-
"""Hold History dashboard service layer."""

from __future__ import annotations

import json
import logging
from datetime import date, datetime, timedelta
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Iterator, Optional

import pandas as pd

from mes_dashboard.core.database import (
    DatabaseCircuitOpenError,
    DatabasePoolExhaustedError,
    read_sql_df,
)
from mes_dashboard.core.redis_client import get_key, get_redis_client
from mes_dashboard.services.filter_cache import get_workcenter_group as _get_wc_group
from mes_dashboard.sql.filters import CommonFilters

logger = logging.getLogger('mes_dashboard.hold_history_service')

_SQL_DIR = Path(__file__).resolve().parent.parent / 'sql' / 'hold_history'
_VALID_HOLD_TYPES = {'quality', 'non-quality', 'all'}
_TREND_CACHE_TTL_SECONDS = 12 * 60 * 60
_TREND_CACHE_KEY_PREFIX = 'hold_history:daily'


@lru_cache(maxsize=16)
def _load_hold_history_sql(name: str) -> str:
    """Load hold history SQL by file name without extension."""
    path = _SQL_DIR / f'{name}.sql'
    if not path.exists():
        raise FileNotFoundError(f'SQL file not found: {path}')

    sql = path.read_text(encoding='utf-8')
    if '{{ NON_QUALITY_REASONS }}' in sql:
        sql = sql.replace('{{ NON_QUALITY_REASONS }}', CommonFilters.get_non_quality_reasons_sql())
    return sql


def _parse_iso_date(value: str) -> date:
    return datetime.strptime(str(value), '%Y-%m-%d').date()


def _format_iso_date(value: date) -> str:
    return value.strftime('%Y-%m-%d')


def _iter_days(start: date, end: date) -> Iterator[date]:
    current = start
    while current <= end:
        yield current
        current += timedelta(days=1)


def _iter_month_starts(start: date, end: date) -> Iterator[date]:
    current = start.replace(day=1)
    while current <= end:
        yield current
        current = (current.replace(day=28) + timedelta(days=4)).replace(day=1)


def _month_end(month_start: date) -> date:
    next_month_start = (month_start.replace(day=28) + timedelta(days=4)).replace(day=1)
    return next_month_start - timedelta(days=1)


def _is_cacheable_month(month_start: date, today: Optional[date] = None) -> bool:
    current = (today or date.today()).replace(day=1)
    previous = (current - timedelta(days=1)).replace(day=1)
    return month_start in {current, previous}


def _trend_cache_key(month_start: date) -> str:
    return get_key(f'{_TREND_CACHE_KEY_PREFIX}:{month_start.strftime("%Y-%m")}')


def _normalize_hold_type(hold_type: Optional[str], default: str = 'quality') -> str:
    normalized = str(hold_type or default).strip().lower()
    if normalized not in _VALID_HOLD_TYPES:
        return default
    return normalized


def _record_type_flags(record_type: Any) -> Dict[str, int]:
    """Convert record_type value(s) to SQL boolean flags."""
    if isinstance(record_type, (list, tuple, set)):
        types = {str(t).strip().lower() for t in record_type}
    else:
        types = {t.strip().lower() for t in str(record_type or 'new').split(',')}
    return {
        'include_new': 1 if 'new' in types else 0,
        'include_on_hold': 1 if 'on_hold' in types else 0,
        'include_released': 1 if 'released' in types else 0,
    }


def _safe_int(value: Any) -> int:
    if value is None:
        return 0
    try:
        if pd.isna(value):
            return 0
    except Exception:
        pass
    try:
        return int(float(value))
    except (TypeError, ValueError):
        return 0


def _safe_float(value: Any) -> float:
    if value is None:
        return 0.0
    try:
        if pd.isna(value):
            return 0.0
    except Exception:
        pass
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.0


def _clean_text(value: Any) -> Optional[str]:
    if value is None:
        return None
    try:
        if pd.isna(value):
            return None
    except Exception:
        pass
    text = str(value).strip()
    return text or None


def _format_datetime(value: Any) -> Optional[str]:
    if value is None:
        return None
    try:
        if pd.isna(value):
            return None
    except Exception:
        pass

    if isinstance(value, datetime):
        dt = value
    elif isinstance(value, pd.Timestamp):
        dt = value.to_pydatetime()
    elif isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        try:
            dt = pd.to_datetime(text).to_pydatetime()
        except Exception:
            return text
    else:
        try:
            dt = pd.to_datetime(value).to_pydatetime()
        except Exception:
            return str(value)

    return dt.strftime('%Y-%m-%d %H:%M:%S')


def _empty_trend_metrics() -> Dict[str, int]:
    return {
        'holdQty': 0,
        'newHoldQty': 0,
        'releaseQty': 0,
        'futureHoldQty': 0,
    }


def _empty_trend_day(day: str) -> Dict[str, Any]:
    return {
        'date': day,
        'quality': _empty_trend_metrics(),
        'non_quality': _empty_trend_metrics(),
        'all': _empty_trend_metrics(),
    }


def _normalize_trend_day(payload: Dict[str, Any], fallback_day: Optional[str] = None) -> Dict[str, Any]:
    day = str(payload.get('date') or fallback_day or '').strip()
    normalized = _empty_trend_day(day)

    for source_key, target_key in (
        ('quality', 'quality'),
        ('non_quality', 'non_quality'),
        ('non-quality', 'non_quality'),
        ('all', 'all'),
    ):
        section = payload.get(source_key)
        if not isinstance(section, dict):
            continue
        normalized[target_key] = {
            'holdQty': _safe_int(section.get('holdQty')),
            'newHoldQty': _safe_int(section.get('newHoldQty')),
            'releaseQty': _safe_int(section.get('releaseQty')),
            'futureHoldQty': _safe_int(section.get('futureHoldQty')),
        }

    return normalized


def _build_month_trend_from_df(df: pd.DataFrame) -> list[Dict[str, Any]]:
    if df is None or df.empty:
        return []

    day_map: Dict[str, Dict[str, Any]] = {}

    for _, row in df.iterrows():
        day = str(row.get('TXN_DATE') or '').strip()
        if not day:
            continue

        if day not in day_map:
            day_map[day] = _empty_trend_day(day)

        hold_type = str(row.get('HOLD_TYPE') or '').strip().lower()
        if hold_type == 'non-quality':
            target_key = 'non_quality'
        elif hold_type in {'quality', 'all'}:
            target_key = hold_type
        else:
            continue

        day_map[day][target_key] = {
            'holdQty': _safe_int(row.get('HOLD_QTY')),
            'newHoldQty': _safe_int(row.get('NEW_HOLD_QTY')),
            'releaseQty': _safe_int(row.get('RELEASE_QTY')),
            'futureHoldQty': _safe_int(row.get('FUTURE_HOLD_QTY')),
        }

    return [day_map[key] for key in sorted(day_map)]


def _query_month_trend(month_start: date) -> list[Dict[str, Any]]:
    month_end = _month_end(month_start)
    sql = _load_hold_history_sql('trend')
    params = {
        'start_date': _format_iso_date(month_start),
        'end_date': _format_iso_date(month_end),
    }
    df = read_sql_df(sql, params)
    return _build_month_trend_from_df(df)


def _get_month_trend_cache(month_start: date) -> Optional[list[Dict[str, Any]]]:
    client = get_redis_client()
    if client is None:
        return None

    key = _trend_cache_key(month_start)
    try:
        payload = client.get(key)
        if not payload:
            return None
        decoded = json.loads(payload)
        if not isinstance(decoded, list):
            return None

        items: list[Dict[str, Any]] = []
        for item in decoded:
            if not isinstance(item, dict):
                continue
            normalized = _normalize_trend_day(item)
            if normalized.get('date'):
                items.append(normalized)
        if not items:
            return None
        return items
    except Exception as exc:
        logger.warning('Failed reading hold-history trend cache key %s: %s', key, exc)
        return None


def _set_month_trend_cache(month_start: date, items: list[Dict[str, Any]]) -> None:
    client = get_redis_client()
    if client is None:
        return

    key = _trend_cache_key(month_start)
    try:
        client.setex(
            key,
            _TREND_CACHE_TTL_SECONDS,
            json.dumps(items, ensure_ascii=False),
        )
    except Exception as exc:
        logger.warning('Failed writing hold-history trend cache key %s: %s', key, exc)


def _get_month_trend_data(month_start: date) -> list[Dict[str, Any]]:
    if _is_cacheable_month(month_start):
        cached = _get_month_trend_cache(month_start)
        if cached is not None:
            return cached

        queried = _query_month_trend(month_start)
        _set_month_trend_cache(month_start, queried)
        return queried

    return _query_month_trend(month_start)


def get_hold_history_trend(start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
    """Get daily trend data for all hold-type variants."""
    try:
        start = _parse_iso_date(start_date)
        end = _parse_iso_date(end_date)
        if end < start:
            return {'days': []}

        day_map: Dict[str, Dict[str, Any]] = {}
        for month_start in _iter_month_starts(start, end):
            month_days = _get_month_trend_data(month_start)
            for item in month_days:
                normalized = _normalize_trend_day(item)
                day = normalized.get('date')
                if day:
                    day_map[day] = normalized

        days: list[Dict[str, Any]] = []
        for current in _iter_days(start, end):
            current_key = _format_iso_date(current)
            days.append(day_map.get(current_key, _empty_trend_day(current_key)))

        return {'days': days}
    except (DatabasePoolExhaustedError, DatabaseCircuitOpenError):
        raise
    except Exception as exc:
        logger.error('Hold history trend query failed: %s', exc)
        return None


def get_hold_history_reason_pareto(
    start_date: str,
    end_date: str,
    hold_type: str,
    record_type: str = 'new',
) -> Optional[Dict[str, Any]]:
    """Get reason Pareto items."""
    try:
        sql = _load_hold_history_sql('reason_pareto')
        params = {
            'start_date': start_date,
            'end_date': end_date,
            'hold_type': _normalize_hold_type(hold_type),
            **_record_type_flags(record_type),
        }
        df = read_sql_df(sql, params)

        items: list[Dict[str, Any]] = []
        if df is not None and not df.empty:
            for _, row in df.iterrows():
                items.append({
                    'reason': _clean_text(row.get('REASON')) or '(未填寫)',
                    'count': _safe_int(row.get('ITEM_COUNT')),
                    'qty': _safe_int(row.get('QTY')),
                    'pct': round(_safe_float(row.get('PCT')), 2),
                    'cumPct': round(_safe_float(row.get('CUM_PCT')), 2),
                })

        return {'items': items}
    except (DatabasePoolExhaustedError, DatabaseCircuitOpenError):
        raise
    except Exception as exc:
        logger.error('Hold history reason pareto query failed: %s', exc)
        return None


def get_hold_history_duration(
    start_date: str,
    end_date: str,
    hold_type: str,
    record_type: str = 'new',
) -> Optional[Dict[str, Any]]:
    """Get hold duration distribution buckets."""
    try:
        sql = _load_hold_history_sql('duration')
        params = {
            'start_date': start_date,
            'end_date': end_date,
            'hold_type': _normalize_hold_type(hold_type),
            **_record_type_flags(record_type),
        }
        df = read_sql_df(sql, params)

        items: list[Dict[str, Any]] = []
        if df is not None and not df.empty:
            for _, row in df.iterrows():
                items.append({
                    'range': _clean_text(row.get('RANGE_LABEL')) or '-',
                    'count': _safe_int(row.get('ITEM_COUNT')),
                    'qty': _safe_int(row.get('QTY')),
                    'pct': round(_safe_float(row.get('PCT')), 2),
                })

        return {'items': items}
    except (DatabasePoolExhaustedError, DatabaseCircuitOpenError):
        raise
    except Exception as exc:
        logger.error('Hold history duration query failed: %s', exc)
        return None


def get_hold_history_list(
    start_date: str,
    end_date: str,
    hold_type: str,
    reason: Optional[str] = None,
    record_type: str = 'new',
    duration_range: Optional[str] = None,
    page: int = 1,
    per_page: int = 50,
) -> Optional[Dict[str, Any]]:
    """Get paginated hold history detail list."""
    try:
        page = max(int(page or 1), 1)
        per_page = max(1, min(int(per_page or 50), 200))
        offset = (page - 1) * per_page

        sql = _load_hold_history_sql('list')
        params = {
            'start_date': start_date,
            'end_date': end_date,
            'hold_type': _normalize_hold_type(hold_type),
            'reason': reason,
            **_record_type_flags(record_type),
            'duration_range': duration_range,
            'offset': offset,
            'limit': per_page,
        }
        df = read_sql_df(sql, params)

        items: list[Dict[str, Any]] = []
        total = 0

        if df is not None and not df.empty:
            for _, row in df.iterrows():
                if total == 0:
                    total = _safe_int(row.get('TOTAL_COUNT'))

                wc_name = _clean_text(row.get('WORKCENTER'))
                wc_group = _get_wc_group(wc_name) if wc_name else None
                items.append({
                    'lotId': _clean_text(row.get('LOT_ID')),
                    'workorder': _clean_text(row.get('WORKORDER')),
                    'product': _clean_text(row.get('PRODUCT')),
                    'workcenter': wc_group or wc_name,
                    'holdReason': _clean_text(row.get('HOLD_REASON')),
                    'qty': _safe_int(row.get('QTY')),
                    'holdDate': _format_datetime(row.get('HOLD_DATE')),
                    'holdEmp': _clean_text(row.get('HOLD_EMP')),
                    'holdComment': _clean_text(row.get('HOLD_COMMENT')),
                    'releaseDate': _format_datetime(row.get('RELEASE_DATE')),
                    'releaseEmp': _clean_text(row.get('RELEASE_EMP')),
                    'releaseComment': _clean_text(row.get('RELEASE_COMMENT')),
                    'holdHours': round(_safe_float(row.get('HOLD_HOURS')), 2),
                    'ncr': _clean_text(row.get('NCR_ID')),
                    'futureHoldComment': _clean_text(row.get('FUTURE_HOLD_COMMENT')),
                })

        total_pages = (total + per_page - 1) // per_page if total > 0 else 1

        return {
            'items': items,
            'pagination': {
                'page': page,
                'perPage': per_page,
                'total': total,
                'totalPages': total_pages,
            },
        }
    except (DatabasePoolExhaustedError, DatabaseCircuitOpenError):
        raise
    except Exception as exc:
        logger.error('Hold history list query failed: %s', exc)
        return None
