﻿import os, uuid, json, aiofiles, asyncio, threading
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Header, WebSocket
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from datetime import datetime
from typing import Dict, Optional, List
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import sys, traceback

# Windows uyumluluğu
if sys.platform.startswith('win'):
    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

app = FastAPI(title='AKIT 7.4 Ultimate Windows Edition')

API_TOKEN = os.getenv('API_TOKEN','demo123')
MAX_CONCURRENT_JOBS = 10
jobs: Dict[str, Dict] = {}
job_queue = asyncio.PriorityQueue()
active_jobs = set()
live_clients = set()

SCRIPT_DIR = 'custom_scripts'
UPLOAD_DIR = 'uploads'
os.makedirs(SCRIPT_DIR, exist_ok=True)
os.makedirs('logs', exist_ok=True)
os.makedirs(UPLOAD_DIR, exist_ok=True)

CUSTOM_COMMANDS = {
    'backup': 'robocopy {source} {dest} /E',
    'list_desktop': 'dir C:\\Users\\{user}\\Desktop'
}

class CommandRequest(BaseModel):
    shell: str
    command: Optional[str]=None
    custom: Optional[str]=None
    params: Optional[Dict[str,str]]=None
    script_name: Optional[str]=None
    timeout: Optional[int]=30
    priority: int=5
    tags: Optional[List[str]]=[]
    schedule: Optional[str]=None

# ----------------- Job worker -----------------
async def job_worker():
    while True:
        priority, job_id, req = await job_queue.get()
        active_jobs.add(job_id)
        await run_job(job_id, req)
        active_jobs.remove(job_id)
        job_queue.task_done()

def log_job(job_id):
    with open(os.path.join('logs','job_history.json'),'a',encoding='utf-8') as f:
        f.write(json.dumps(jobs[job_id],ensure_ascii=False)+'\n')

# ----------------- run_job -----------------
import subprocess

def run_job_sync(job_id: str, req: CommandRequest):
    jobs[job_id]['status'] = 'running'
    jobs[job_id]['stdout'] = ''
    jobs[job_id]['stderr'] = ''
    returncode = -1

    try:
        cmd = None
        cmd_str = req.command or req.custom

        # custom komut
        if req.custom and req.custom in CUSTOM_COMMANDS:
            cmd_str = CUSTOM_COMMANDS[req.custom].format(**(req.params or {}))

        # script veya shell
        if req.script_name:
            script_path = os.path.join(SCRIPT_DIR, req.script_name)
            if not os.path.exists(script_path):
                jobs[job_id]['stderr'] = f'Script not found: {req.script_name}'
                jobs[job_id]['status'] = 'error'
                jobs[job_id]['returncode'] = -1
                return
            if script_path.endswith('.py'):
                python_exe = os.path.join('backend','python','python.exe')
                cmd = [python_exe, script_path]
            elif script_path.endswith('.bat'):
                cmd = ['cmd.exe','/c', script_path]
            elif script_path.endswith('.ps1'):
                cmd = ['powershell','-NoProfile','-ExecutionPolicy','Bypass','-File', script_path]
        else:
            if req.shell.lower() == 'python':
                python_exe = os.path.join('backend','python','python.exe')
                cmd = [python_exe, '-c', cmd_str]
            elif req.shell.lower() == 'cmd':
                cmd = ['cmd.exe','/c', cmd_str]
            elif req.shell.lower() == 'powershell':
                cmd = ['powershell','-NoProfile','-Command', cmd_str]

        print(f"[{job_id}] Running command: {cmd}")

        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=req.timeout
        )

        jobs[job_id]['stdout'] = proc.stdout
        jobs[job_id]['stderr'] = proc.stderr
        returncode = proc.returncode
        jobs[job_id]['returncode'] = returncode
        jobs[job_id]['status'] = 'completed' if returncode==0 else 'error'

    except subprocess.TimeoutExpired:
        jobs[job_id]['stderr'] += '\nTimeout'
        jobs[job_id]['status'] = 'error'
        jobs[job_id]['returncode'] = -1

    except Exception as e:
        jobs[job_id]['stderr'] += '\nException:\n' + traceback.format_exc()
        jobs[job_id]['status'] = 'error'
        jobs[job_id]['returncode'] = -1

    jobs[job_id]['finished_at'] = str(datetime.now())
    log_job(job_id)

# ----------------- Live WS -----------------
class LiveChangeHandler(FileSystemEventHandler):
    def __init__(self, loop):
        self.loop = loop
    def on_any_event(self, event):
        data = {'event': event.event_type, 'path': event.src_path, 'is_dir': event.is_directory, 'time': str(datetime.now())}
        asyncio.run_coroutine_threadsafe(notify_clients(data), self.loop)

async def notify_clients(data):
    disconnected=[]
    for ws in live_clients:
        try:
            await ws.send_text(json.dumps(data))
        except:
            disconnected.append(ws)
    for ws in disconnected:
        live_clients.remove(ws)

@app.websocket('/ws/live')
async def websocket_live(ws: WebSocket):
    await ws.accept()
    live_clients.add(ws)
    try:
        while True:
            await ws.receive_text()
    except:
        live_clients.remove(ws)

@app.on_event('startup')
async def startup():
    for _ in range(MAX_CONCURRENT_JOBS):
        asyncio.create_task(job_worker())
    app.mount('/frontend', StaticFiles(directory='../frontend'), name='frontend')
    loop = asyncio.get_event_loop()
    handler = LiveChangeHandler(loop)
    observer = Observer()
    observer.schedule(handler, SCRIPT_DIR, recursive=True)
    observer.schedule(handler, UPLOAD_DIR, recursive=True)
    threading.Thread(target=observer.start, daemon=True).start()

# ----------------- Helper -----------------
def validate_path(path:str):
    if not os.path.exists(path):
        raise HTTPException(404,'Path not found')

# ----------------- Endpoints -----------------
@app.post('/run')
async def run_command(req:CommandRequest, x_token:str=Header(...), wait:bool=True):
    if x_token!=API_TOKEN: raise HTTPException(401)
    if not req.command and not req.custom and not req.script_name:
        raise HTTPException(400,'command, custom or script_name required')
    job_id=str(uuid.uuid4())[:8]
    jobs[job_id]={'id':job_id,'shell':req.shell.lower(),'command':req.command or req.custom,
                  'script_name':req.script_name,'priority':req.priority,'tags':req.tags,
                  'timeout':req.timeout,'schedule':req.schedule,'status':'queued',
                  'stdout':'','stderr':'','returncode':None,'created_at':str(datetime.now())}
    await job_queue.put((req.priority, job_id, req))
    if wait:
        while jobs[job_id]['status'] not in ['completed','timeout','error']:
            await asyncio.sleep(0.2)
        return jobs[job_id]
    return {'job_id':job_id,'status':'queued'}

@app.get('/files')
def list_files(path:str='C:\\', x_token:str=Header(...), filter_ext:Optional[str]=None, search:Optional[str]=None):
    if x_token!=API_TOKEN: raise HTTPException(401)
    try:
        items=[]
        for name in os.listdir(path):
            full=os.path.join(path,name)
            if search and search.lower() not in name.lower(): continue
            if filter_ext and not name.lower().endswith(filter_ext.lower()): continue
            items.append({'name':name,'is_dir':os.path.isdir(full),
                          'size':os.path.getsize(full) if os.path.isfile(full) else 0,
                          'modified':os.path.getmtime(full)})
        return {'path':path,'items':items}
    except Exception as e: raise HTTPException(400,str(e))

@app.post('/upload')
async def upload_file(file: UploadFile = File(...), local_path: Optional[str] = Form(None), x_token:str=Header(...)):
    if x_token!=API_TOKEN: raise HTTPException(401)
    out_path=os.path.join(UPLOAD_DIR, file.filename)
    async with aiofiles.open(out_path,'wb') as f:
        while content := await file.read(1024*1024):
            await f.write(content)
    log_entry={'filename': file.filename,'uploaded_at': str(datetime.now()),'local_path': local_path}
    async with aiofiles.open(os.path.join('logs','upload_history.json'),'a',encoding='utf-8') as f:
        await f.write(json.dumps(log_entry,ensure_ascii=False)+'\n')
    return {'status':'ok','filename':file.filename,'local_path':local_path}

@app.get('/download')
def download_file(filename:str, x_token:str=Header(...)):
    if x_token!=API_TOKEN: raise HTTPException(401)
    path=os.path.join(UPLOAD_DIR,filename)
    if not os.path.exists(path): raise HTTPException(404)
    return FileResponse(path)
