import os
import re
import subprocess
import base64
import shlex
import gzip
import platform
import shutil
import tarfile
import tempfile
import zipfile
import requests
from datetime import datetime

from flask import Blueprint, request, jsonify

from ctrl import api_error, api_ok
from ctrl.task_ctrl import create_task, update_task
from lib import systemd_utils


clash_bp = Blueprint('clash', __name__)


DEFAULT_SERVICE = 'mihomo.service'
SERVICE_CANDIDATES = ['clash.service', 'clash-meta.service', 'mihomo.service']


def _arch_linux_name():
    m = (platform.machine() or '').lower()
    if m in ('x86_64', 'amd64'):
        return 'amd64'
    if m in ('aarch64', 'arm64'):
        return 'arm64'
    if m.startswith('armv7'):
        return 'armv7'
    if m in ('i386', 'i686', 'x86'):
        return '386'
    return 'amd64'


def _run_shell(task_id, cmd: str, timeout=None):
    update_task(task_id, status='running', message=cmd)
    r = subprocess.run(['bash', '-lc', cmd], capture_output=True, text=True, timeout=timeout)
    if r.returncode != 0:
        err = (r.stderr or r.stdout or '').strip()
        raise RuntimeError(err or ('command failed: ' + cmd))
    return r.stdout


def _mihomo_bin_path():
    home = os.path.expanduser('~')
    for p in [f'{home}/.local/bin/mihomo', '/usr/local/mihomo/mihomo', '/usr/local/bin/mihomo', '/usr/bin/mihomo', '/usr/local/bin/clash', '/usr/bin/clash']:
        if os.path.exists(p) and os.access(p, os.X_OK):
            return p
    found = shutil.which('mihomo') or shutil.which('clash')
    return found or ''


def _clash_install_state():
    installed = bool(_mihomo_bin_path()) or any(_systemctl_user_show(u).get('available') for u in SERVICE_CANDIDATES)
    return {'installed': bool(installed), 'bin': _mihomo_bin_path()}


@clash_bp.route('/api/clash/install_state')
def api_clash_install_state():
    return api_ok(_clash_install_state())


def _systemctl_show_props(unit: str, props: str, user: bool = True):
    try:
        cmd = ['systemctl']
        if user:
            cmd.append('--user')
        cmd.extend(['show', unit, '--no-pager', f'--property={props}'])
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
    except Exception:
        return None

    if result.returncode != 0:
        return None

    out = {}
    for line in (result.stdout or '').splitlines():
        if '=' in line:
            k, v = line.split('=', 1)
            k = (k or '').strip()
            v = (v or '').strip()
            if k:
                out[k] = v
    return out


def _systemctl_user_show(unit: str):
    """获取 systemd 服务状态"""
    props = _systemctl_show_props(
        unit,
        'Id,Description,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,ExecStart',
        user=True,
    )
    if not props:
        props = _systemctl_show_props(
            unit,
            'Id,Description,LoadState,ActiveState,SubState,UnitFileState,FragmentPath,ExecStart',
            user=False,
        )
    if not props:
        return {'available': False, 'error': 'systemctl failed'}

    # 服务不存在（not-found）时认为不可用
    load_state = props.get('LoadState', '')
    if load_state == 'not-found':
        return {'available': False, 'error': 'service not found'}

    running = props.get('ActiveState') == 'active' and props.get('SubState') == 'running'
    return {
        'available': True,
        'id': props.get('Id') or unit,
        'description': props.get('Description') or '',
        'load_state': load_state,
        'active_state': props.get('ActiveState') or '',
        'sub_state': props.get('SubState') or '',
        'unit_file_state': props.get('UnitFileState') or '',
        'fragment_path': props.get('FragmentPath') or '',
        'exec_start': props.get('ExecStart') or '',
        'running': running,
    }


def _extract_config_path_from_execstart(exec_start: str):
    text = (exec_start or '').strip()
    if not text:
        return None

    argv = None
    m = re.search(r'argv\[\]=([^;]+)', text)
    if m:
        argv = m.group(1).strip()
    else:
        argv = text

    try:
        parts = shlex.split(argv, posix=True)
    except Exception:
        parts = argv.split()

    if not parts:
        return None

    def norm(p: str):
        if not p:
            return None
        p = os.path.expanduser(p)
        return p

    for i, a in enumerate(parts):
        if a in ('-f', '-c', '--config', '--config-file') and i + 1 < len(parts):
            return norm(parts[i + 1])
        if a.startswith('--config=') or a.startswith('--config-file='):
            return norm(a.split('=', 1)[1])

    for i, a in enumerate(parts):
        if a in ('-d', '--dir', '--home') and i + 1 < len(parts):
            base_dir = norm(parts[i + 1])
            if not base_dir:
                continue
            for name in ('config.yaml', 'config.yml'):
                candidate = os.path.join(base_dir, name)
                if os.path.exists(candidate):
                    return candidate
            return os.path.join(base_dir, 'config.yaml')
        if a.startswith('--dir=') or a.startswith('--home='):
            base_dir = norm(a.split('=', 1)[1])
            if not base_dir:
                continue
            for name in ('config.yaml', 'config.yml'):
                candidate = os.path.join(base_dir, name)
                if os.path.exists(candidate):
                    return candidate
            return os.path.join(base_dir, 'config.yaml')

    return None


def _find_config():
    for unit in SERVICE_CANDIDATES:
        st = _systemctl_user_show(unit)
        if not st.get('available'):
            continue
        p = _extract_config_path_from_execstart(st.get('exec_start') or '')
        if p:
            return p
    st = _systemctl_user_show(DEFAULT_SERVICE)
    if st.get('available'):
        return _extract_config_path_from_execstart(st.get('exec_start') or '')
    return None


def _parse_clash_yaml(content: str):
    """解析 Clash YAML 配置"""
    text = content or ''
    out = {
        'proxies': [],
        'proxy_groups': [],
        'proxy_providers': [],  # 新增
        'rules_count': 0,
        'ports': {},
    }
    
    lines = text.split('\n')
    in_proxies = False
    in_proxy_providers = False
    in_proxy_groups = False
    current_provider = None
    
    for i, line in enumerate(lines):
        line_stripped = line.strip()
        
        if line_stripped.startswith('proxies:'):
            if current_provider:
                out['proxy_providers'].append(current_provider)
                current_provider = None
            in_proxies = True
            in_proxy_providers = False
            in_proxy_groups = False
            continue
        if 'proxy-providers:' in line_stripped or 'Providers' in line_stripped:
            in_proxies = False
            in_proxy_providers = True
            in_proxy_groups = False
            current_provider = None
            continue
        if 'proxy-groups:' in line_stripped or 'Groups' in line_stripped:
            if current_provider:
                out['proxy_providers'].append(current_provider)
                current_provider = None
            in_proxies = False
            in_proxy_providers = False
            in_proxy_groups = True
            continue
        if line_stripped.startswith('rules:') or line_stripped.startswith('Rules:'):
            if current_provider:
                out['proxy_providers'].append(current_provider)
                current_provider = None
            in_proxies = False
            in_proxy_providers = False
            in_proxy_groups = False
            # 统计规则数量
            remaining = [l.strip() for l in lines[i+1:] if l.strip() and not l.strip().startswith('#')]
            out['rules_count'] = len(remaining)
            continue
        
        # 提取 proxy-providers
        if in_proxy_providers:
            # 检测退出
            if line_stripped.startswith('proxies:') or line_stripped.startswith('proxy-groups:') or line_stripped.startswith('rules:'):
                if current_provider:
                    out['proxy_providers'].append(current_provider)
                    current_provider = None
                in_proxy_providers = False
                continue
            
            if not line_stripped or line_stripped.startswith('#'):
                continue
            
            # 键值对格式
            key_match = re.match(r'^(\S[^:]*?)\s*:$', line_stripped)
            if key_match:
                name = key_match.group(1).strip().strip('"').strip("'")
                # 排除关键字
                if name and name not in ['proxy-providers', 'Proxy-providers', 'proxies', 'proxy-groups', 'rules', 'Rules']:
                    if current_provider:
                        out['proxy_providers'].append(current_provider)
                    current_provider = {'name': name, 'url': '', 'type': '', 'path': ''}
                    continue
            
            # 提取属性
            if current_provider:
                url_match = re.search(r'url\s*:\s*(.+)', line_stripped)
                if url_match:
                    current_provider['url'] = url_match.group(1).strip().strip('"').strip("'")
                type_match = re.search(r'type\s*:\s*(.+)', line_stripped)
                if type_match:
                    current_provider['type'] = type_match.group(1).strip().strip('"').strip("'")
                path_match = re.search(r'path\s*:\s*(.+)', line_stripped)
                if path_match:
                    current_provider['path'] = path_match.group(1).strip().strip('"').strip("'")
                interval_match = re.search(r'interval\s*:\s*(\d+)', line_stripped)
                if interval_match:
                    current_provider['interval'] = interval_match.group(1)
        
        # 提取节点
        if in_proxies and line_stripped.startswith('- '):
            # 提取节点名称
            name_match = re.search(r'name\s*:\s*["\']?([^"\'\n,]+)["\']?', line_stripped)
            if name_match:
                name = name_match.group(1).strip()
                # 提取类型
                type_match = re.search(r'type\s*:\s*["\']?([^"\'\n,]+)["\']?', line_stripped)
                node_type = type_match.group(1).strip() if type_match else 'unknown'
                out['proxies'].append({'name': name, 'type': node_type})
        
        # 提取代理组
        if in_proxy_groups and line_stripped.startswith('- name:'):
            name = line_stripped.replace('- name:', '').strip().strip('"').strip("'")
            if name:
                out['proxy_groups'].append(name)
    
    # 最后一个 provider
    if current_provider:
        out['proxy_providers'].append(current_provider)
    
    # 提取端口信息
    port_match = re.search(r'(?:listen|port)\s*:\s*(\d+)', text)
    socks_match = re.search(r'socks-port\s*:\s*(\d+)', text)
    mixed_match = re.search(r'mixed-port\s*:\s*(\d+)', text)
    
    out['ports'] = {
        'http': port_match.group(1) if port_match else None,
        'socks': socks_match.group(1) if socks_match else None,
        'mixed': mixed_match.group(1) if mixed_match else None,
    }
    
    return out


@clash_bp.route('/api/clash/state')
def api_clash_state():
    """获取 Clash 状态和配置"""
    config_path = _find_config()
    config = {'present': False, 'path': config_path or '', 'proxies': [], 'proxy_groups': [], 'rules_count': 0, 'ports': {}}
    
    try:
        if config_path and os.path.exists(config_path):
            with open(config_path, 'r', encoding='utf-8') as f:
                content = f.read()
            parsed = _parse_clash_yaml(content)
            config = {
                'present': True,
                'path': config_path,
                'proxies': parsed['proxies'][:100],  # 限制数量
                'proxy_groups': parsed['proxy_groups'],
                'rules_count': parsed['rules_count'],
                'ports': parsed['ports'],
                'size': len(content),
            }
    except Exception as e:
        config = {'present': False, 'path': config_path or '', 'error': str(e)}

    service = None
    for svc in SERVICE_CANDIDATES:
        st = _systemctl_user_show(svc)
        if st.get('available'):
            service = st
            break
    if not service:
        service = _systemctl_user_show(DEFAULT_SERVICE)
    
    return api_ok({'config': config, 'service': service})


@clash_bp.route('/api/clash/control', methods=['POST'])
def api_clash_control():
    """控制 Clash 服务"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    action = (data.get('action') or '').strip().lower()
    if action not in {'start', 'stop', 'restart'}:
        return api_error('Invalid action', status=400)
    
    # 尝试多个服务名
    service = DEFAULT_SERVICE
    for svc in ['clash.service', 'clash-meta.service', 'mihomo.service']:
        st = _systemctl_user_show(svc)
        if st.get('available'):
            service = svc
            break
    
    result = systemd_utils.control_systemd_service(service, action)
    if isinstance(result, dict) and result.get('success'):
        return api_ok({'ok': True})
    
    return api_error(result.get('message') or 'Error', status=500)


@clash_bp.route('/api/clash/subscribe', methods=['POST'])
def api_clash_subscribe():
    """更新 Clash 订阅"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    url = (data.get('url') or '').strip()
    if not url:
        return api_error('Missing URL', status=400)
    
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    
    try:
        # 下载订阅
        headers = {
            'User-Agent': 'ClashforWindows/0.20.0',
            'Accept': 'text/yaml,text/plain,*/*',
        }
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()
        content = response.text.strip()
        
        # 尝试 Base64 解码
        try:
            decoded = base64.b64decode(content).decode('utf-8')
            if decoded.strip().startswith('proxies:') or decoded.strip().startswith('- '):
                content = decoded
        except:
            pass
        
        # 读取现有配置
        with open(config_path, 'r', encoding='utf-8') as f:
            existing = f.read()
        
        # 备份
        backup_path = config_path + '.bak.' + datetime.now().strftime('%Y%m%d%H%M%S')
        with open(backup_path, 'w', encoding='utf-8') as f:
            f.write(existing)
        
        # 合并配置
        lines = existing.split('\n')
        new_lines = []
        in_proxies = False
        proxies_started = False
        
        for line in lines:
            new_lines.append(line)
            if line.strip().startswith('proxies:'):
                in_proxies = True
                proxies_started = True
                # 提取订阅中的节点
                sub_nodes = []
                for sub_line in content.split('\n'):
                    sub_line_stripped = sub_line.strip()
                    if sub_line_stripped.startswith('- '):
                        sub_nodes.append(sub_line)
                    elif sub_line_stripped and not sub_line_stripped.startswith('#'):
                        if sub_line_stripped.startswith('proxies:'):
                            continue
                        if not sub_line_stripped.startswith('- ') and ':' in sub_line_stripped:
                            # 是配置项不是节点
                            in_proxies = False
                            break
                # 添加订阅节点
                for node in sub_nodes:
                    new_lines.append('  ' + node)
        
        with open(config_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(new_lines))
        
        return api_ok({
            'success': True,
            'path': config_path,
            'backup': backup_path,
            'message': '订阅更新成功',
        })
        
    except requests.exceptions.RequestException as e:
        return api_error(f'下载失败: {str(e)}', status=500)
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/provider/add', methods=['POST'])
def api_clash_provider_add():
    """添加订阅 provider"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    url = (data.get('url') or '').strip()
    name = (data.get('name') or '').strip() or '机场订阅'
    interval = data.get('interval', 3600)  # 默认1小时
    
    if not url:
        return api_error('Missing URL', status=400)
    
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    
    try:
        # 读取现有配置
        with open(config_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 备份
        backup_path = config_path + '.bak.' + datetime.now().strftime('%Y%m%d%H%M%S')
        with open(backup_path, 'w', encoding='utf-8') as f:
            f.write(content)
        
        # 添加 proxy-providers 区块
        lines = content.split('\n')
        new_lines = []
        has_proxy_providers = False
        provider_path = os.path.join(os.path.dirname(config_path), 'proxy-providers', f'{name}.yaml')
        
        # 确保目录存在
        os.makedirs(os.path.dirname(provider_path), exist_ok=True)
        
        for line in lines:
            new_lines.append(line)
            if line.strip().startswith('proxy-providers:') or line.strip().startswith('Proxy-providers:'):
                has_proxy_providers = True
                new_lines.append(f'  {name}:')
                new_lines.append(f'    type: http')
                new_lines.append(f'    url: "{url}"')
                new_lines.append(f'    interval: {interval}')
                new_lines.append(f'    path: ./{os.path.basename(provider_path)}')
        
        if not has_proxy_providers:
            # 在 mode 后面添加 proxy-providers
            for i, line in enumerate(new_lines):
                if line.strip().startswith('mode:'):
                    new_lines.insert(i + 1, '')
                    new_lines.insert(i + 2, 'proxy-providers:')
                    new_lines.insert(i + 3, f'  {name}:')
                    new_lines.insert(i + 4, f'    type: http')
                    new_lines.insert(i + 5, f'    url: "{url}"')
                    new_lines.insert(i + 6, f'    interval: {interval}')
                    new_lines.insert(i + 7, f'    path: ./{os.path.basename(provider_path)}')
                    break
        
        with open(config_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(new_lines))
        
        return api_ok({
            'success': True,
            'path': config_path,
            'backup': backup_path,
            'message': f'添加订阅成功: {name}',
        })
        
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/provider/delete', methods=['POST'])
def api_clash_provider_delete():
    """删除订阅 provider"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    name = (data.get('name') or '').strip()
    if not name:
        return api_error('Missing provider name', status=400)
    
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 备份
        backup_path = config_path + '.bak.' + datetime.now().strftime('%Y%m%d%H%M%S')
        with open(backup_path, 'w', encoding='utf-8') as f:
            f.write(content)
        
        # 删除 provider
        lines = content.split('\n')
        new_lines = []
        in_provider = False
        provider_indent = 0
        
        for line in lines:
            line_stripped = line.strip()
            
            # 检测是否进入目标 provider (支持两种格式)
            # 格式1: "- name: xxx"
            # 格式2: "provider_name:"
            is_provider_start = False
            if line_stripped.startswith(f'- name:') and name in line:
                is_provider_start = True
            elif line_stripped.endswith(':') and not line_stripped.startswith('-'):
                # 键值对格式
                provider_name = line_stripped.rstrip(':').strip()
                if provider_name == name:
                    is_provider_start = True
            
            if is_provider_start:
                in_provider = True
                provider_indent = len(line) - len(line.lstrip())
                continue
            
            # 如果在目标 provider 内
            if in_provider:
                current_indent = len(line) - len(line.lstrip())
                # 如果缩进小于等于 provider 缩进，说明已离开
                if line.strip() and current_indent <= provider_indent:
                    in_provider = False
                    new_lines.append(line)
                else:
                    continue  # 跳过 provider 内的行
            else:
                new_lines.append(line)
        
        with open(config_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(new_lines))
        
        return api_ok({
            'success': True,
            'path': config_path,
            'backup': backup_path,
            'message': f'删除订阅成功: {name}',
        })
        
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/provider/refresh', methods=['POST'])
def api_clash_provider_refresh():
    """手动刷新订阅 provider"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    name = (data.get('name') or '').strip()
    if not name:
        return api_error('Missing provider name', status=400)
    
    # 调用 mihomo API 刷新 (不编码URL)
    try:
        import json
        # 直接使用原始字符串，不进行URL编码
        response = requests.put(
            f'http://127.0.0.1:9090/providers/proxies/{name}',
            timeout=60
        )
        if response.status_code in (200, 204):
            return api_ok({'success': True, 'message': f'刷新成功: {name}'})
        else:
            return api_error(f'刷新失败: {response.status_code} {response.text}', status=500)
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/proxies', methods=['GET'])
def api_clash_proxies():
    """获取代理列表和当前选择"""
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        parsed = _parse_clash_yaml(content)
        
        # 尝试解析当前选中的节点（通过查找 proxy-groups 中的 selected）
        current_selection = {}
        lines = content.split('\n')
        in_proxy_groups = False
        
        for line in lines:
            line_stripped = line.strip()
            if line_stripped.startswith('proxy-groups:') or 'Groups' in line_stripped:
                in_proxy_groups = True
                continue
            if in_proxy_groups:
                if line_stripped.startswith('- name:'):
                    group_name = line_stripped.replace('- name:', '').strip().strip('"').strip("'")
                    current_selection[group_name] = None
                elif 'proxies:' in line_stripped or 'proxy:' in line_stripped:
                    # 提取当前选中
                    for group_name in current_selection:
                        if group_name not in current_selection or current_selection[group_name] is None:
                            match = re.search(rf'{re.escape(group_name)}["\']?\s*,\s*proxies:\s*\[[^\]]*["\']?([^"\',\]]+)', line)
                            if match:
                                current_selection[group_name] = match.group(1).strip().strip('"').strip("'")
        
        return api_ok({
            'proxies': parsed['proxies'],
            'proxy_groups': parsed['proxy_groups'],
            'proxy_providers': parsed['proxy_providers'],
            'current_selection': current_selection,
            'ports': parsed['ports'],
            'rules_count': parsed['rules_count'],
        })
        
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/switch', methods=['POST'])
def api_clash_switch():
    """切换代理节点（通过修改配置）"""
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    
    group_name = (data.get('group') or '').strip()
    proxy_name = (data.get('proxy') or '').strip()
    
    if not group_name or not proxy_name:
        return api_error('Missing group or proxy', status=400)
    
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 查找并替换代理组中的选中节点
        # 模式: name: xxx, proxies: [..., "新节点", ...]
        pattern = rf'({re.escape(group_name)}["\']?,?\s*proxies:\s*\[)([^"\']*"{re.escape(proxy_name)}["\']?)([^\]]*\])'
        
        def replacer(match):
            prefix = match.group(1)
            old_proxy = match.group(2)
            suffix = match.group(3)
            # 移除旧的选中节点，添加新的
            proxies = [p.strip().strip('"').strip("'") for p in (prefix + old_proxy + suffix).split(',') if p.strip() and p.strip() != old_proxy.strip()]
            # 将新节点放到第一位
            proxies = [proxy_name] + [p for p in proxies if p != proxy_name]
            return prefix + ', '.join([f'"{p}"' for p in proxies]) + suffix
        
        # 简化处理：只替换包含该组名的行中的节点名
        new_content = content
        lines = content.split('\n')
        for i, line in enumerate(lines):
            if group_name in line and ('proxies:' in line or 'proxy:' in line):
                # 找到这行，替换节点名
                # 查找当前选中的节点并替换
                for proxy in re.findall(r'"([^"]+)"', line):
                    if proxy != proxy_name:
                        new_line = line.replace(f'"{proxy}"', f'"{proxy_name}"', 1)
                        lines[i] = new_line
                        break
        
        new_content = '\n'.join(lines)
        
        # 备份
        backup_path = config_path + '.switch.' + datetime.now().strftime('%Y%m%d%H%M%S')
        with open(backup_path, 'w', encoding='utf-8') as f:
            f.write(content)
        
        with open(config_path, 'w', encoding='utf-8') as f:
            f.write(new_content)
        
        return api_ok({
            'success': True,
            'message': f'已将 {group_name} 切换到 {proxy_name}',
            'backup': backup_path,
        })
        
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/config', methods=['GET'])
def api_clash_config():
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            content = f.read()
        return api_ok({'content': content, 'path': config_path})
    except Exception as e:
        return api_error(str(e), status=500)


@clash_bp.route('/api/clash/config', methods=['POST'])
def api_clash_config_save():
    config_path = _find_config()
    if not config_path or not os.path.exists(config_path):
        return api_error('Config file not found', status=404)
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return api_error('Invalid JSON', status=400)
    content = data.get('content')
    if not isinstance(content, str):
        return api_error('Missing content', status=400)
    try:
        backup_path = config_path + '.backup.' + datetime.now().strftime('%Y%m%d%H%M%S')
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                old = f.read()
            with open(backup_path, 'w', encoding='utf-8') as f:
                f.write(old)
        except Exception:
            backup_path = None
        with open(config_path, 'w', encoding='utf-8') as f:
            f.write(content)
        return api_ok({'success': True, 'path': config_path, 'backup': backup_path})
    except Exception as e:
        return api_error(str(e), status=500)


def _ensure_mihomo_config():
    cfg_dir = os.path.expanduser('~/.config/mihomo')
    os.makedirs(cfg_dir, exist_ok=True)
    cfg_path = os.path.join(cfg_dir, 'config.yaml')
    if os.path.exists(cfg_path):
        return cfg_dir, cfg_path
    content = '\n'.join([
        'mixed-port: 7890',
        'allow-lan: true',
        'mode: rule',
        'log-level: info',
        'external-controller: 127.0.0.1:9090',
        '',
        'proxies: []',
        'proxy-groups: []',
        'rules: []',
        '',
    ])
    with open(cfg_path, 'w', encoding='utf-8') as f:
        f.write(content)
    return cfg_dir, cfg_path


def _ensure_mihomo_service(task_id):
    unit_dir = os.path.expanduser('~/.config/systemd/user')
    os.makedirs(unit_dir, exist_ok=True)
    unit_path = os.path.join(unit_dir, 'mihomo.service')
    cfg_dir, _ = _ensure_mihomo_config()
    content = '\n'.join([
        '[Unit]',
        'Description=Mihomo (Clash Meta)',
        'After=network.target',
        '',
        '[Service]',
        'Type=simple',
        f'ExecStart={home}/.local/bin/mihomo -d {cfg_dir}',
        'Restart=on-failure',
        'RestartSec=2',
        '',
        '[Install]',
        'WantedBy=default.target',
        '',
    ])
    with open(unit_path, 'w', encoding='utf-8') as f:
        f.write(content)
    _run_shell(task_id, 'systemctl --user daemon-reload', timeout=20)
    _run_shell(task_id, 'systemctl --user enable mihomo.service', timeout=20)
    _run_shell(task_id, 'systemctl --user restart mihomo.service', timeout=30)


def _download_mihomo_binary(task_id):
    arch = _arch_linux_name()
    update_task(task_id, status='running', message='fetching mihomo release')
    resp = requests.get('https://api.github.com/repos/MetaCubeX/mihomo/releases/latest', timeout=15)
    resp.raise_for_status()
    data = resp.json() or {}
    assets = data.get('assets') or []
    candidates = []
    for a in assets:
        name = (a.get('name') or '').lower()
        if 'linux' not in name:
            continue
        if arch not in name:
            continue
        if name.endswith(('.gz', '.tar.gz', '.zip')):
            candidates.append(a)
    if not candidates:
        for a in assets:
            name = (a.get('name') or '').lower()
            if 'linux' in name and arch in name:
                candidates.append(a)
    if not candidates:
        raise RuntimeError('no compatible mihomo asset found')
    asset = candidates[0]
    url = asset.get('browser_download_url') or ''
    if not url:
        raise RuntimeError('asset missing download url')
    filename = asset.get('name') or 'mihomo'

    update_task(task_id, status='running', message='downloading ' + filename)
    with requests.get(url, stream=True, timeout=60) as r:
        r.raise_for_status()
        with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as tf:
            for chunk in r.iter_content(chunk_size=1024 * 256):
                if chunk:
                    tf.write(chunk)
            file_path = tf.name

    home = os.path.expanduser('~')
    install_dir = f'{home}/.local/bin'
    os.makedirs(install_dir, exist_ok=True)
    out_path = f'{install_dir}/mihomo'
    update_task(task_id, status='running', message='installing mihomo')
    try:
        if filename.endswith('.gz') and not filename.endswith('.tar.gz'):
            with gzip.open(file_path, 'rb') as f_in, open(out_path, 'wb') as f_out:
                shutil.copyfileobj(f_in, f_out)
        elif filename.endswith('.tar.gz'):
            with tarfile.open(file_path, 'r:gz') as tar:
                member = None
                for m in tar.getmembers():
                    if os.path.basename(m.name) in ('mihomo', 'clash') and m.isfile():
                        member = m
                        break
                if not member:
                    raise RuntimeError('mihomo binary not found in tar')
                extracted = tar.extractfile(member)
                if not extracted:
                    raise RuntimeError('extract failed')
                with open(out_path, 'wb') as f_out:
                    f_out.write(extracted.read())
        elif filename.endswith('.zip'):
            with zipfile.ZipFile(file_path, 'r') as zf:
                bin_name = None
                for n in zf.namelist():
                    if os.path.basename(n) in ('mihomo', 'clash'):
                        bin_name = n
                        break
                if not bin_name:
                    raise RuntimeError('mihomo binary not found in zip')
                with zf.open(bin_name) as f_in, open(out_path, 'wb') as f_out:
                    shutil.copyfileobj(f_in, f_out)
        else:
            shutil.copyfile(file_path, out_path)
        os.chmod(out_path, 0o755)
    finally:
        try:
            os.remove(file_path)
        except Exception:
            pass


def _clash_uninstall(task_id):
    try:
        _run_shell(task_id, 'systemctl --user stop mihomo.service', timeout=20)
    except Exception:
        pass
    try:
        _run_shell(task_id, 'systemctl --user disable mihomo.service', timeout=20)
    except Exception:
        pass
    unit_path = os.path.expanduser('~/.config/systemd/user/mihomo.service')
    try:
        if os.path.exists(unit_path):
            os.remove(unit_path)
    except Exception:
        pass
    try:
        _run_shell(task_id, 'systemctl --user daemon-reload', timeout=20)
    except Exception:
        pass
    try:
        if os.path.isdir('/usr/local/mihomo'):
            shutil.rmtree('/usr/local/mihomo', ignore_errors=True)
        home = os.path.expanduser('~')
        bin_path = f'{home}/.local/bin/mihomo'
        if os.path.exists(bin_path):
            os.remove(bin_path)
    except Exception:
        pass


@clash_bp.route('/api/clash/install', methods=['POST'])
def api_clash_install():
    def _do(task_id):
        state = _clash_install_state()
        if state.get('installed'):
            return
        _download_mihomo_binary(task_id)
        _ensure_mihomo_service(task_id)

    task_id = create_task(_do, name='clash install')
    return api_ok({'taskId': task_id})


@clash_bp.route('/api/clash/reinstall', methods=['POST'])
def api_clash_reinstall():
    def _do(task_id):
        _clash_uninstall(task_id)
        _download_mihomo_binary(task_id)
        _ensure_mihomo_service(task_id)

    task_id = create_task(_do, name='clash reinstall')
    return api_ok({'taskId': task_id})


@clash_bp.route('/api/clash/uninstall', methods=['POST'])
def api_clash_uninstall():
    def _do(task_id):
        _clash_uninstall(task_id)

    task_id = create_task(_do, name='clash uninstall')
    return api_ok({'taskId': task_id})
