# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import io
import threading
import webbrowser
import os
import subprocess
import signal
import sys
import locale
import json
import shutil
import uuid
import time


PORT = 9998  # 静态文件服务端口
API_PORT = 23456  # 接收 POST 请求服务端口

# index.html 左侧文件管理器用到的目录约定
PROTECTED_TEMPLATES = ('4.0c.html',)  # 内置模板, 不允许删除(4.0c 起只保留一个最新样板)
RESERVED_TOP_DIRS = ('js', 'geogebra')  # 文件管理器不可见/不可操作的目录
TRASH_DIR = os.path.join('js', 'trash')
MANIFEST_PATH = os.path.join(TRASH_DIR, 'manifest.json')

try:
    # Python 3
    from http.server import SimpleHTTPRequestHandler, BaseHTTPRequestHandler, HTTPServer
    from urllib.parse import unquote, urlparse, parse_qs
    import socketserver
    PY3 = True
except ImportError:
    # Python 2
    from SimpleHTTPServer import SimpleHTTPRequestHandler
    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
    from urlparse import urlparse, parse_qs
    from urllib import unquote
    import SocketServer as socketserver
    PY3 = False


# 构建 js/htmlfiles.js，包含根目录及其任意深度子文件夹下的所有 html 文件列表(相对路径, 用 / 分隔)
# js/、geogebra/ 是资源目录, 递归时直接剪掉, 避免把离线内核里的 html(如 geogebra/5.0/index.html)也列进去
def generate_html_files_js():
    html_files = []
    for root, dirs, files in os.walk('.'):
        if root == '.':
            dirs[:] = [d for d in dirs if d not in RESERVED_TOP_DIRS]
        rel_root = os.path.relpath(root, '.')
        for f in files:
            if not f.lower().endswith('.html'):
                continue
            if rel_root == '.' and f.lower() == 'index.html':
                continue
            rel_path = f if rel_root == '.' else '/'.join(rel_root.split(os.sep) + [f])
            html_files.append("'{}'".format(rel_path))
    html_files.sort()
    js_content = "let html_files = [{}];".format(', '.join(html_files))
    js_dir = 'js'
    if not os.path.exists(js_dir):
        os.makedirs(js_dir)
    with io.open(os.path.join(js_dir, 'htmlfiles.js'), 'w', encoding='utf-8') as f:
        f.write(js_content)


# ============================================================
# index.html 左侧文件管理器: list/mkdir/newFile/delete/restore
# ============================================================

def _reject_unsafe(rel_path):
    """校验 rel_path 解析后仍落在 ggbx 根目录内, 且不指向 js/、geogebra/ 这两个保留目录; 非法则抛 ValueError"""
    if not rel_path or rel_path.startswith('/') or rel_path.startswith('\\') or ':' in rel_path:
        raise ValueError(u'非法路径')
    top = rel_path.replace('\\', '/').split('/')[0]
    if top in RESERVED_TOP_DIRS:
        raise ValueError(u'该目录由系统保留, 不能操作')
    root_abs = os.path.abspath('.')
    target_abs = os.path.abspath(os.path.join(root_abs, rel_path))
    if target_abs != root_abs and not target_abs.startswith(root_abs + os.sep):
        raise ValueError(u'非法路径')
    return target_abs


def _to_rel_posix(abs_path):
    rel = os.path.relpath(abs_path, os.path.abspath('.'))
    return rel.replace(os.sep, '/')


def _load_manifest():
    if not os.path.exists(MANIFEST_PATH):
        return []
    try:
        with io.open(MANIFEST_PATH, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return []


def _save_manifest(entries):
    if not os.path.exists(TRASH_DIR):
        os.makedirs(TRASH_DIR)
    with io.open(MANIFEST_PATH, 'w', encoding='utf-8') as f:
        f.write(json.dumps(entries, ensure_ascii=False, indent=2))


def _build_tree(dir_abs):
    entries = []
    try:
        names = sorted(os.listdir(dir_abs))
    except OSError:
        return entries
    for name in names:
        full = os.path.join(dir_abs, name)
        rel = _to_rel_posix(full)
        if rel in RESERVED_TOP_DIRS or rel == 'index.html':
            continue
        if os.path.isdir(full):
            entries.append({
                'path': rel, 'name': name, 'isDir': True,
                'children': _build_tree(full),
            })
        elif name.lower().endswith('.html'):
            entries.append({'path': rel, 'name': name, 'isDir': False})
    return entries


def action_list():
    tree = _build_tree(os.path.abspath('.'))
    trash = _load_manifest()
    return {'success': True, 'tree': tree, 'trash': trash}


def action_mkdir(payload):
    rel = payload.get('path', '') or ''
    target = _reject_unsafe(rel)
    if os.path.exists(target):
        return {'success': False, 'error': u'目标已存在'}
    os.makedirs(target)
    return {'success': True}


def action_new_file(payload):
    rel = payload.get('path', '') or ''
    if not rel.lower().endswith('.html'):
        return {'success': False, 'error': u'文件名必须以 .html 结尾'}
    target = _reject_unsafe(rel)
    if os.path.exists(target):
        return {'success': False, 'error': u'目标已存在'}
    template = os.path.abspath('4.0c.html')
    if not os.path.exists(template):
        return {'success': False, 'error': u'模板文件 4.0c.html 不存在'}
    parent = os.path.dirname(target)
    if parent and not os.path.exists(parent):
        os.makedirs(parent)
    shutil.copyfile(template, target)
    return {'success': True}


def action_delete(payload):
    rel = payload.get('path', '') or ''
    if rel in PROTECTED_TEMPLATES:
        return {'success': False, 'error': u'该文件是内置模板, 不能删除'}
    target = _reject_unsafe(rel)
    if not os.path.exists(target):
        return {'success': False, 'error': u'目标不存在'}
    trash_id = uuid.uuid4().hex[:12]
    basename = os.path.basename(target)
    if not os.path.exists(TRASH_DIR):
        os.makedirs(TRASH_DIR)
    dest = os.path.join(TRASH_DIR, '{}__{}'.format(trash_id, basename))
    is_dir = os.path.isdir(target)
    shutil.move(target, dest)
    manifest = _load_manifest()
    manifest.append({
        'id': trash_id,
        'originalPath': rel,
        'isDir': is_dir,
        'deletedAt': int(time.time() * 1000),
    })
    _save_manifest(manifest)
    return {'success': True, 'id': trash_id}


def action_restore(payload):
    trash_id = payload.get('id', '') or ''
    manifest = _load_manifest()
    entry = None
    for e in manifest:
        if e.get('id') == trash_id:
            entry = e
            break
    if not entry:
        return {'success': False, 'error': u'回收站记录不存在'}
    original_rel = entry['originalPath']
    original_abs = _reject_unsafe(original_rel)
    if os.path.exists(original_abs):
        return {'success': False, 'error': u'原路径已被占用, 无法还原'}
    basename = os.path.basename(original_abs)
    trash_src = os.path.join(TRASH_DIR, '{}__{}'.format(trash_id, basename))
    if not os.path.exists(trash_src):
        return {'success': False, 'error': u'回收站文件缺失'}
    parent = os.path.dirname(original_abs)
    if parent and not os.path.exists(parent):
        os.makedirs(parent)
    shutil.move(trash_src, original_abs)
    manifest = [e for e in manifest if e.get('id') != trash_id]
    _save_manifest(manifest)
    return {'success': True}


def action_rename(payload):
    rel = payload.get('path', '') or ''
    new_name = payload.get('newName', '') or ''
    if rel in PROTECTED_TEMPLATES:
        return {'success': False, 'error': u'该文件是内置模板, 不能重命名'}
    if not new_name or new_name in ('.', '..') or '/' in new_name or '\\' in new_name or ':' in new_name:
        return {'success': False, 'error': u'新名称不合法'}
    target = _reject_unsafe(rel)
    if not os.path.exists(target):
        return {'success': False, 'error': u'目标不存在'}
    is_dir = os.path.isdir(target)
    if not is_dir and not new_name.lower().endswith('.html'):
        return {'success': False, 'error': u'文件名必须以 .html 结尾'}
    new_target = os.path.join(os.path.dirname(target), new_name)
    if os.path.exists(new_target):
        return {'success': False, 'error': u'目标名称已存在'}
    os.rename(target, new_target)
    return {'success': True}


def _unique_copy_name(dir_abs, base, ext):
    candidate = base + '_copy' + ext
    n = 2
    while os.path.exists(os.path.join(dir_abs, candidate)):
        candidate = base + '_copy' + str(n) + ext
        n += 1
    return candidate


def action_duplicate(payload):
    rel = payload.get('path', '') or ''
    target = _reject_unsafe(rel)
    if not os.path.exists(target):
        return {'success': False, 'error': u'目标不存在'}
    dir_abs = os.path.dirname(target)
    basename = os.path.basename(target)
    if os.path.isdir(target):
        new_name = _unique_copy_name(dir_abs, basename, '')
        shutil.copytree(target, os.path.join(dir_abs, new_name))
    else:
        name, ext = os.path.splitext(basename)
        new_name = _unique_copy_name(dir_abs, name, ext)
        shutil.copyfile(target, os.path.join(dir_abs, new_name))
    return {'success': True, 'name': new_name}


def action_move(payload):
    rel = payload.get('path', '') or ''
    target_dir_rel = payload.get('targetDir', '') or ''
    if rel in PROTECTED_TEMPLATES:
        return {'success': False, 'error': u'该文件是内置模板, 不能移动'}
    target = _reject_unsafe(rel)
    if not os.path.exists(target):
        return {'success': False, 'error': u'目标不存在'}
    target_dir_abs = _reject_unsafe(target_dir_rel) if target_dir_rel else os.path.abspath('.')
    if not os.path.isdir(target_dir_abs):
        return {'success': False, 'error': u'目标文件夹不存在'}
    if os.path.isdir(target) and (target_dir_abs == target or target_dir_abs.startswith(target + os.sep)):
        return {'success': False, 'error': u'不能移动到自身或子文件夹中'}
    new_target = os.path.join(target_dir_abs, os.path.basename(target))
    if os.path.abspath(new_target) == os.path.abspath(target):
        return {'success': False, 'error': u'目标位置与原位置相同'}
    if os.path.exists(new_target):
        return {'success': False, 'error': u'目标位置已存在同名文件/文件夹'}
    shutil.move(target, new_target)
    return {'success': True}


# 静态文件服务
def start_simple_server():
    handler = SimpleHTTPRequestHandler
    httpd = socketserver.TCPServer(("localhost", PORT), handler)
    print("Serving static files on http://localhost:%d/" % PORT)
    webbrowser.open("http://localhost:%d/index.html" % PORT)
    httpd.serve_forever()


# 接收 POST 请求写文件的服务
class RequestHandler(BaseHTTPRequestHandler):
    def _set_headers(self, content_length):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.send_header('Content-Length', str(content_length))
        self.end_headers()

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def _send_json(self, obj, status=200):
        encoded = json.dumps(obj, ensure_ascii=False).encode('utf-8')
        self.send_response(status)
        self.send_header('Content-type', 'application/json; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.send_header('Content-Length', str(len(encoded)))
        self.end_headers()
        self.wfile.write(encoded)

    def _run_action(self, action, payload):
        try:
            if action == 'list':
                return self._send_json(action_list())
            if action == 'mkdir':
                return self._send_json(action_mkdir(payload))
            if action == 'newFile':
                return self._send_json(action_new_file(payload))
            if action == 'delete':
                return self._send_json(action_delete(payload))
            if action == 'restore':
                return self._send_json(action_restore(payload))
            if action == 'rename':
                return self._send_json(action_rename(payload))
            if action == 'duplicate':
                return self._send_json(action_duplicate(payload))
            if action == 'move':
                return self._send_json(action_move(payload))
            return self._send_json({'success': False, 'error': u'未知操作'}, status=400)
        except Exception as e:
            return self._send_json({'success': False, 'error': str(e)}, status=400)

    def do_GET(self):
        parsed_url = urlparse(self.path)
        query = parse_qs(parsed_url.query)
        action = query.get('action', [''])[0]
        if action:
            return self._run_action(action, {})
        message = u"This is a POST-only server. Use POST with ?file=yourpath"
        encoded = message.encode('utf-8')
        self._set_headers(len(encoded))
        self.wfile.write(encoded)

    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        if isinstance(body, bytes):
            body = body.decode('utf-8')

        parsed_url = urlparse(self.path)
        query = parse_qs(parsed_url.query)

        # 左侧文件管理器: list/mkdir/newFile/delete/restore, 不影响下面原有的整份覆写文件行为
        action = query.get('action', [''])[0]
        if action:
            try:
                payload = json.loads(body) if body else {}
            except Exception:
                payload = {}
            return self._run_action(action, payload)

        filepath = query.get('file', [''])[0]

        if filepath:
            filepath = unquote(filepath)
            try:
                with io.open(filepath, 'w', encoding='utf-8') as f:
                    f.write(body)
            except Exception as e:
                self.send_error(500, str(e))
                return

        response = u'true'
        encoded = response.encode('utf-8')
        self._set_headers(len(encoded))
        self.wfile.write(encoded)


def start_api_server():
    server_address = ('localhost', API_PORT)
    httpd = HTTPServer(server_address, RequestHandler)
    print('Serving API on http://localhost:%d/' % API_PORT)
    httpd.serve_forever()


def print_utf8(s):
    # 兼容py2和py3的print，避免编码问题
    if sys.version_info[0] < 3:
        # python2: 输出时先编码为utf-8
        print(s.encode('utf-8'))
    else:
        print(s)

def kill_process(process_name):
    try:
        # /F 强制结束, /IM 按名称, /T 结束子进程
        subprocess.check_call(['taskkill', '/F', '/IM', process_name, '/T'])
        print_utf8(u"已终止进程：{}".format(process_name))
    except subprocess.CalledProcessError:
        print_utf8(u"未找到或无法终止进程：{}".format(process_name))

def killcmd():
    print_utf8(u"正在终止所有 cmd.exe 和 powershell.exe 进程...")

    # 结束进程列表
    processes = ['pwsh.exe', 'powershell.exe', 'cmd.exe']
    for p in processes:
        kill_process(p)

    print_utf8(u"操作完成。")


def kill_process_on_port(port):
    try:
        if os.name == 'nt':
            import locale
            encoding = locale.getpreferredencoding()
            cmd_find = ['netstat', '-ano']
            output = subprocess.check_output(cmd_find)
            output = output.decode(encoding, errors='ignore')
            for line in output.splitlines():
                if 'LISTENING' in line and ':%d' % port in line:
                    parts = line.strip().split()
                    if len(parts) >= 5:
                        pid = int(parts[-1])
                        print("Killing process on port %d with PID %d (Windows)" % (port, pid))
                        subprocess.call(['taskkill', '/PID', str(pid), '/F'])
                    return
        else:
            cmd = "lsof -i :%d -t" % port
            output = subprocess.check_output(cmd, shell=True)
            output = output.decode('utf-8', errors='ignore').strip()
            if output:
                for pid in output.splitlines():
                    print("Killing process on port %d with PID %s" % (port, pid))
                    os.kill(int(pid), signal.SIGKILL)
    except Exception as e:
        print("No need to kill port %d: %s" % (port, e))
        if os.name == 'nt':
            killcmd()

if __name__ == '__main__':
    kill_process_on_port(PORT)
    kill_process_on_port(API_PORT)
    os.chdir(os.path.dirname(os.path.abspath(__file__)))  # 切换到脚本目录
    generate_html_files_js()

    # 启动两个线程
    t1 = threading.Thread(target=start_simple_server)
    t2 = threading.Thread(target=start_api_server)

    t1.daemon = True
    t2.daemon = True

    t1.start()
    t2.start()

    # 保持主线程运行
    t1.join()
    t2.join()
