"""MCP Tool Lab, plan management, and utility views."""
import json
import asyncio
import threading
import traceback
import logging
from pathlib import Path

from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

from analysis.appdata import ensure_appdata_dir
from analysis.models import AnalysisRun
from .pipeline import _background_process
from .helpers import (
    _PLANS_DIR, _AUTO_INDEX_PATH, _MAX_AUTO_PLANS,
    _read_auto_index, _write_auto_index, _add_to_auto_index,
)

logger = logging.getLogger(__name__)


def tool_lab(request):
    """List all available MCP tools with descriptions and input schemas."""
    from analysis.tool_registry import get_tools_meta
    tools = get_tools_meta()
    # Group tools by category
    core_names = {'profile_data', 'build_entity_profiles', 'compute_scores',
                  'run_clustering', 'extract_features', 'detect_anomalies',
                  'visualize_anomalies'}
    diag_names = {'validate_data', 'explore_distributions', 'find_outliers',
                  'diagnose_clustering', 'compare_datasets', 'export_debug_sample',
                  'repair_schema'}
    analysis_names = {'analyze_patterns', 'analyze_temporal', 'analyze_tls_health',
                      'analyze_geo_distribution', 'analyze_entity_detail'}

    core_tools, diag_tools, analysis_tools, other_tools = [], [], [], []
    for t in tools:
        if t.name in core_names:
            core_tools.append(t)
        elif t.name in diag_names:
            diag_tools.append(t)
        elif t.name in analysis_names:
            analysis_tools.append(t)
        else:
            other_tools.append(t)

    # Get list of datasets for parameter autofill
    from analysis.session_store import SessionStore
    store = SessionStore()
    ds_list = store.list_datasets() if hasattr(store, 'list_datasets') else []

    import json as _json
    # Serialize tools metadata for JS
    meta_list = []
    for t in tools:
        meta_list.append({
            'name': t.name,
            'description': t.description,
            'inputSchema': t.inputSchema,
        })
    return render(request, 'analysis/tool_lab.html', {
        'core_tools': core_tools,
        'diag_tools': diag_tools,
        'analysis_tools': analysis_tools,
        'other_tools': other_tools,
        'datasets': ds_list,
        'tools_meta': _json.dumps(meta_list, ensure_ascii=False),
    })


@csrf_exempt
def tool_lab_run(request):
    """Execute a single MCP tool via AJAX POST."""
    if request.method != 'POST':
        return JsonResponse({'error': 'POST required'}, status=405)
    import json as _json
    try:
        body = _json.loads(request.body)
    except Exception:
        logger.error("tool_lab_run failed: {}".format(traceback.format_exc()))
        return JsonResponse({'error': 'Invalid JSON body'}, status=400)

    tool_name = body.get('tool')
    params = body.get('params', {})
    if not tool_name:
        return JsonResponse({'error': 'Missing tool name'}, status=400)

    # ── Backend validation: check required params + dataset existence ──
    from analysis.tool_registry import handle_call, get_tools_meta
    from analysis.session_store import SessionStore
    tools_meta = get_tools_meta()
    tool_schema = None
    for t in tools_meta:
        if t.name == tool_name:
            tool_schema = t.inputSchema
            break
    if tool_schema:
        required_fields = tool_schema.get('required', [])
        for field in required_fields:
            val = params.get(field)
            if val is None or (isinstance(val, str) and val.strip() == ''):
                return JsonResponse({'error': f'Missing required parameter: {field}'}, status=400)
            # If field is a dataset_id, verify it exists in the store
            if 'dataset' in field.lower():
                store = SessionStore()
                if not store.get_dataset(str(val)):
                    return JsonResponse({'error': f'Dataset not found: {val}'}, status=400)

    result = asyncio.run(handle_call(tool_name, params))
    return JsonResponse({'status': 'ok', 'result': result})


@csrf_exempt
def reload_run_data(request):
    """Reload a run's data from csv_glob or sqlite_table into SessionStore."""
    if request.method != 'POST':
        return JsonResponse({'error': 'POST required'}, status=405)
    import json as _json
    try:
        body = _json.loads(request.body)
    except Exception:
        logger.error("reload_run_data failed: {}".format(traceback.format_exc()))
        return JsonResponse({'error': 'Invalid JSON body'}, status=400)

    display_id = body.get('display_id')
    if not display_id:
        return JsonResponse({'error': 'Missing display_id'}, status=400)

    from analysis.models import AnalysisRun
    from analysis.data_loader import load_csv_directory, load_from_db

    run = get_object_or_404(AnalysisRun, display_id=display_id)

    # Determine dataset_id (same keying as manual_page)
    ds_id = f'upload_{display_id}' if run.run_type == 'upload' else f'run_{display_id}'

    from analysis.session_store import SessionStore
    store = SessionStore()
    # Check if already loaded
    existing = store.get_dataset(ds_id)
    if existing is not None:
        return JsonResponse({'status': 'ok', 'dataset_id': ds_id, 'reloaded': False})

    # Try loading from sqlite_table first, then csv_glob
    lf = None
    schema = {}
    row_count = 0
    file_count = 0

    if run.sqlite_table:
        try:
            lf = load_from_db(run.sqlite_table)
            if lf is not None:
                import polars as pl
                row_count = lf.select(pl.len()).collect(streaming=True)[0, 0]
        except Exception as exc:
            logging.getLogger(__name__).warning(f'Failed to load from sqlite_table: {exc}')

    if lf is None and run.csv_glob:
        try:
            from pathlib import Path
            csv_path = Path(run.csv_glob)
            if csv_path.is_file() or csv_path.is_dir() or csv_path.parent.is_dir():
                lf, schema, row_count, file_count, _ = load_csv_directory(run.csv_glob)
        except Exception as exc:
            logging.getLogger(__name__).warning(f'Failed to load from csv_glob: {exc}')

    if lf is None:
        return JsonResponse({'error': 'No data source available (upload dir or sqlite table gone)'}, status=404)

    store.store_dataset(ds_id, lf, schema=schema, metadata={
        'row_count': row_count,
        'file_count': file_count,
        'csv_glob': run.csv_glob or '',
        'sqlite_table': run.sqlite_table or '',
    })

    return JsonResponse({
        'status': 'ok',
        'dataset_id': ds_id,
        'reloaded': True,
        'row_count': row_count,
        'columns': list(schema.keys()),
    })


@csrf_exempt
def tool_plan(request):
    """Save, list, load, delete, or pin/unpin an analysis plan."""
    import json as _j
    if request.method == 'GET':
        name = request.GET.get('name')
        if name:
            # Auto plans stored with _auto_ prefix
            if name.startswith('_auto_'):
                path = _PLANS_DIR / f'{name}.json'
            else:
                path = _PLANS_DIR / f'{name}.json'
            if not path.exists():
                return JsonResponse({'error': f'Plan "{name}" not found'}, status=404)
            try:
                data = _j.loads(path.read_text(encoding='utf-8'))
                return JsonResponse(data)
            except Exception as e:
                return JsonResponse({'error': str(e)}, status=500)

        # List all plans (regular + auto)
        auto_index = _read_auto_index()
        auto_index_map = {e.get('file', ''): e.get('pinned', False) for e in auto_index}
        auto_names = set()
        plans = []
        for p in sorted(_PLANS_DIR.glob('*.json')):
            # Skip index file and auto plans (handled separately)
            if p.name == '_auto_index.json':
                continue
            if p.name.startswith('_auto_'):
                auto_names.add(p.name)
                continue
            try:
                content = _j.loads(p.read_text(encoding='utf-8'))
                steps = len(content.get('steps', []))
                plans.append({'name': p.stem, 'steps': steps})
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                plans.append({'name': p.stem, 'steps': 0})

        # Add auto plans with markers
        auto_plans = []
        for auto_name in sorted(auto_names):
            try:
                path = _PLANS_DIR / auto_name
                content = _j.loads(path.read_text(encoding='utf-8'))
                steps = len(content.get('steps', []))
                auto_plans.append({
                    'name': auto_name.replace('.json', ''),
                    'steps': steps,
                    'auto': True,
                    'pinned': auto_index_map.get(auto_name, False),
                })
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                auto_plans.append({
                    'name': auto_name.replace('.json', ''),
                    'steps': 0,
                    'auto': True,
                    'pinned': auto_index_map.get(auto_name, False),
                })
        # Interleave: auto plans first, then regular
        return JsonResponse({'plans': auto_plans + plans})

    elif request.method == 'POST':
        try:
            body = _j.loads(request.body)
            # Pin/unpin toggle for auto plans
            if 'pin' in body and body.get('name', '').startswith('_auto_'):
                filename = body['name'].replace('.json', '') + '.json' \
                    if not body['name'].endswith('.json') else body['name']
                index = _read_auto_index()
                new_pinned = bool(body['pin'])
                found = False
                for entry in index:
                    if entry.get('file') == filename:
                        entry['pinned'] = new_pinned
                        found = True
                        break
                if not found and new_pinned:
                    index.append({'file': filename, 'pinned': True})
                _write_auto_index(index)
                return JsonResponse({
                    'status': 'pinned' if new_pinned else 'unpinned',
                    'name': body['name'],
                    'pinned': new_pinned,
                })

            # Regular save
            name = body.get('name', '').strip()
            steps = body.get('steps', [])
            if not name:
                return JsonResponse({'error': 'Missing plan name'}, status=400)
            path = _PLANS_DIR / f'{name}.json'
            path.write_text(_j.dumps({'name': name, 'steps': steps}, ensure_ascii=False, indent=2), encoding='utf-8')
            return JsonResponse({'status': 'saved', 'name': name, 'steps': len(steps)})
        except Exception as e:
            return JsonResponse({'error': str(e)}, status=500)

    elif request.method == 'DELETE':
        name = request.GET.get('name', '').strip()
        if not name:
            return JsonResponse({'error': 'Missing plan name'}, status=400)
        path = _PLANS_DIR / f'{name}.json'
        if path.exists():
            path.unlink()
        # Also remove from auto index if applicable
        if name.startswith('_auto_'):
            filename = name + '.json' if not name.endswith('.json') else name
            index = _read_auto_index()
            index = [e for e in index if e.get('file') != filename]
            _write_auto_index(index)
        return JsonResponse({'status': 'deleted', 'name': name})

    return JsonResponse({'error': 'Method not allowed'}, status=405)


# ── Filter endpoint for manual analysis page ──────────────────────────

@csrf_exempt
def apply_filter(request):
    """POST {dataset_id, filters, logic} → apply filter_data tool → return filtered_X id."""
    if request.method != 'POST':
        return JsonResponse({'error': 'POST required'}, status=405)
    import json as _json
    body = _json.loads(request.body)
    dataset_id = body.get('dataset_id')
    filters = body.get('filters', [])
    logic = body.get('logic', 'and')

    if not dataset_id:
        return JsonResponse({'error': 'dataset_id is required'})
    if not filters:
        return JsonResponse({'error': 'filters is required'})

    try:
        from analysis.tool_registry import _handle_filter_data
        result = asyncio.run(_handle_filter_data(
            dataset_id=dataset_id,
            filters=filters,
            logic=logic,
        ))
        if 'error' in result:
            return JsonResponse({'error': result['error']})

        filtered_id = result.get('filtered_dataset_id', '')
        row_count = result.get('row_count', 0)

        # Use the unique flt_xxx ID from _handle_filter_data — never rename
        # to filtered_{num} which would collide on repeated filters of the same source.
        return JsonResponse({
            'dataset_id': filtered_id,
            'row_count': row_count,
        })
    except Exception as e:
        logger.error(f'[apply_filter] {traceback.format_exc()}')
        return JsonResponse({'error': str(e)})


@csrf_exempt
def retry_run(request, display_id):
    """Reset a failed run and re-trigger based on run_type."""
    if request.method != 'POST':
        return JsonResponse({'error': 'POST required'}, status=405)

    run = get_object_or_404(AnalysisRun, display_id=display_id)
    if run.status != 'failed':
        return JsonResponse({'error': '只能重试失败的分析运行'}, status=400)

    run.status = 'pending'
    run.error_message = ''
    run.progress_pct = 0
    run.progress_msg = ''
    run.save(update_fields=['status', 'error_message', 'progress_pct', 'progress_msg'])

    if run.run_type == 'upload' and run.csv_glob:
        upload_dir = Path(run.csv_glob).parent
        expected_base = ensure_appdata_dir() / 'data' / 'uploads'
        if not str(upload_dir.resolve()).startswith(str(expected_base.resolve())):
            return JsonResponse({'error': 'Invalid upload path'}, status=400)
        if upload_dir.is_dir():
            t = threading.Thread(
                target=_background_process,
                args=(run.id, upload_dir),
                daemon=True,
            )
            t.start()
        else:
            run.error_message = '上传数据目录已不存在，请重新上传'
            run.status = 'failed'
            run.save(update_fields=['status', 'error_message'])

    return redirect('analysis:run_detail', display_id=run.display_id)
