﻿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')

async def run_job(job_id: str, req: CommandRequest):
    jobs[job_id]['status'] = 'running'
    try:
        cmd = None
        cmd_str = req.command or req.custom

        # custom komut desteği
        if req.custom:
            template = CUSTOM_COMMANDS.get(req.custom)
            if template:
                params = req.params or {}
                cmd_str = template.format(**params)

        # script çalıştırma
        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', '-ExecutionPolicy', 'Bypass', '-File', script_path]
            elif script_path.endswith('.bat'):
                cmd = ['cmd.exe', '/c', script_path]
            else:
                raise Exception('Unsupported script type')
        else:
            # Shell seçimine göre
            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', cmd_str]
            elif req.shell.lower() == 'cmd':
                cmd = ['cmd.exe', '/c', cmd_str]   # ✅ Windows için internal komutları destekler
            elif req.shell.lower() == 'powershell':
                cmd = ['powershell', '-NoProfile', '-Command', cmd_str]
            else:
                raise Exception('Unsupported shell')

        # process çalıştır
      
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        print(process)
        try:
            stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=req.timeout)
            returncode = process.returncode
        except asyncio.TimeoutError:
            process.kill()
            stdout, stderr = await process.communicate()
            returncode = -1

        jobs[job_id]['stdout'] = stdout.decode(errors='replace')
        jobs[job_id]['stderr'] = stderr.decode(errors='replace')
        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)
