﻿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

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

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')

import os
import asyncio
from datetime import datetime

async def run_job(job_id: str, req: CommandRequest):
    jobs[job_id]['status'] = 'running'
    jobs[job_id]['stdout'] = ''
    jobs[job_id]['stderr'] = ''
    returncode = -1

    try:
        # Komut oluşturma
        cmd = None
        cmd_str = req.command or req.custom

        if req.custom:
            template = CUSTOM_COMMANDS.get(req.custom)
            if template:
                params = req.params or {}
                cmd_str = template.format(**params)

        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'):
                cmd = [os.path.join('backend', 'python', 'python.exe'), script_path]
            elif script_path.endswith('.ps1'):
                cmd = ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script_path]
            elif script_path.endswith('.bat'):
                cmd = ['cmd.exe', '/c', script_path]
            else:
                raise Exception('Unsupported script type')
        else:
            if req.shell.lower() == 'python':
                cmd = [os.path.join(r'D:\__sistem__\Masaustu\FikoYT.Yeni.Cag\docker\fikoyt-file-manager\AKIT7_4\backend\python\python.exe'), '-c',  f'"{cmd_str}"']
            elif req.shell.lower() == 'cmd':
                cmd = ['cmd.exe', '/c', cmd_str]
            elif req.shell.lower() == 'powershell':
                cmd = ['powershell', '-NoProfile', '-Command', cmd_str]
            else:
                raise Exception('Unsupported shell')

        print(f"Running command: {cmd}")

        # Process başlat
        process = await asyncio.create_subprocess_exec(
            *cmd, capture_output=True, text=True,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )

        # Anlık stdout/stderr okuma
        async def read_stream(stream, key):
            while True:
                line = await stream.readline()
                if not line:
                    break
                decoded = line.decode(errors='replace')
                jobs[job_id][key] += decoded
                print(decoded, end='')  # anlık print

        try:
            await asyncio.wait_for(
                asyncio.gather(
                    read_stream(process.stdout, 'stdout'),
                    read_stream(process.stderr, 'stderr')
                ),
                timeout=req.timeout
            )
            returncode = await process.wait()
        except asyncio.TimeoutError:
            process.kill()
            await process.wait()
            returncode = -1

        jobs[job_id]['returncode'] = returncode
        jobs[job_id]['status'] = 'completed' if returncode == 0 else 'error'
        jobs[job_id]['finished_at'] = str(datetime.now())

    except Exception as e:
        jobs[job_id]['stderr'] += f'\nException: {e}'
        jobs[job_id]['status'] = 'error'
        jobs[job_id]['returncode'] = -1
        jobs[job_id]['finished_at'] = str(datetime.now())

    log_job(job_id)

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()

def validate_path(path:str):
    if not os.path.exists(path):
        raise HTTPException(404,'Path not found')

@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)
