import os,sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ["KERAS_BACKEND"] = "jax"
from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY, LOG_PATH, MODEL_DIR
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
from datetime import datetime
from rich import print
from fastapi import FastAPI, Depends, HTTPException, status, File, UploadFile, Response
from fastapi.security import APIKeyHeader
import uvicorn
from uuid import UUID
from v2realbot.utils.ilog import get_log_window
from v2realbot.common.model import RunManagerRecord, StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveView, RunArchiveViewPagination, RunArchiveDetail, Bar, RunArchiveChange, TestList, ConfigItem, InstantIndicator, DataTablesRequest, AnalyzerInputs
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query
from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from v2realbot.enums.enums import Env, Mode
from typing import Annotated
import os
import uvicorn
import orjson
from queue import Queue, Empty
from threading import Thread
import asyncio
from v2realbot.common.db import insert_queue, insert_conn, pool
from v2realbot.utils.utils import json_serial, send_to_telegram, zoneNY, zonePRG
from v2realbot.utils.sysutils import get_environment
from uuid import uuid4
from sqlite3 import OperationalError
from time import sleep
import v2realbot.reporting.metricstools as mt
from v2realbot.reporting.metricstoolsimage import generate_trading_report_image
from traceback import format_exc
#from v2realbot.reporting.optimizecutoffs import find_optimal_cutoff
import v2realbot.reporting.analyzer as ci
import shutil
from starlette.responses import JSONResponse
import mlroom
import mlroom.utils.mlutils as ml
from typing import List
import v2realbot.controller.run_manager as rm
import v2realbot.scheduler.ap_scheduler as aps
import re
import v2realbot.controller.configs as cf
import v2realbot.controller.services as cs
#from async io import Queue, QueueEmpty
#              
# install()
# ic.configureOutput(includeContext=True)
# def threadName():
#     return '%s |> ' % str(current_thread().name)
# ic.configureOutput(prefix=threadName)
# ic.disable()
"""""   
Main entry point of the bot. Starts strategies according to config file, each
in separate thread.
   
CONF:
{'general': {'make_network_connection': True, 'ping_time': 1200},
            'strategies': [{'name': 'Dokupovaci 1', 'symbol': 'BAC'},
                           {'name': 'Vykladaci', 'symbol': 'year'}]}
"""""

        # <link href="https://unpkg.com/tabulator-tables/dist/css/tabulator.min.css" rel="stylesheet">
        # <script type="text/javascript" src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"></script>
 

X_API_KEY = APIKeyHeader(name='X-API-Key')
  
def api_key_auth(api_key: str = Depends(X_API_KEY)):
    if api_key != WEB_API_KEY:
        raise HTTPException( 
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Forbidden"
        )  
     
app = FastAPI()
root = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="static")
app.mount("/media", StaticFiles(directory=str(MEDIA_DIRECTORY)), name="media")
#app.mount("/", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="www")

security = HTTPBasic()

def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)]
):
    if not (credentials.username == "david") or not (credentials.password == "david"):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Basic"},
        )

async def get_api_key(
    websocket: WebSocket,
    session: Annotated[str | None, Cookie()] = None,
    api_key: Annotated[str | None, Query()] = None,
):
    if api_key != WEB_API_KEY:
        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
    return session or api_key

#TODO predelat z Async?
@app.get("/static")
async def get(username: Annotated[str, Depends(get_current_username)]):
    return FileResponse("index.html")

@app.websocket("/runners/{runner_id}/ws")
async def websocket_endpoint(
    *, 
    websocket: WebSocket,
    runner_id: str,
    api_key: Annotated[str, Depends(get_api_key)],
):
    await websocket.accept()
    if not cs.is_runner_running(runner_id):
        #await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA, reason="Strat not running")
        raise WebSocketException(code=status.WS_1003_UNSUPPORTED_DATA, reason="Runner not running.")
        return
    else:
        print("stratin exists")
        q: Queue = Queue()
        await cs.runner_realtime_on(id=runner_id, rtqueue=q)

        # tx task; reads data from queue and sends to websocket
        async def websocket_tx_task(ws, _q):
            print("Starting WS tx...")
            
            while True:
                try:            
                    data = _q.get(timeout=10)
                    if data=="break":
                        break
                    await ws.send_text(data)
                    print("WSTX thread received data") #,data)
                except Empty:
                    print("WSTX thread Heartbeat. No data received from queue.")
                    continue
                except WebSocketDisconnect:
                    print("WSTX thread disconnected - terminating tx job")
                    break

            print("WSTX thread terminated")

        def websocket_tx_task_wrapper(ws, _q):
            asyncio.run(websocket_tx_task(ws, _q))

        ws_tx_thread = Thread(target=websocket_tx_task_wrapper, args = (websocket, q,))
        ws_tx_thread.start()
        try:
            while True:
                    data = await websocket.receive_text()
                    print(f"WS RX: {data}")
                    # data = q.get()
                    # print("WSGOTDATA",data)
                #await websocket.receive_text()
                # await websocket.send_text(
                #     f"Session cookie or query token value is: {cookie_or_token}"
                # )
                # data = {'high': 195, 
                #                     'low': 180,
                #                     'volume': 123,
                #                     'close': 185,
                #                     'hlcc4': 123,
                #                     'open': 190,
                #                     'time': "2019-05-25",
                #                     'trades':123,
                #                     'resolution':123,
                #                     'confirmed': 123,
                #                     'vwap': 123,
                #                     'updated': 123,
                #                     'index': 123}
                #print("WSRT received data", data)
                #await websocket.send_text(data)
        except WebSocketDisconnect:
            print("CLIENT DISCONNECTED for", runner_id)
        finally:
            q.put("break")
            await cs.runner_realtime_off(runner_id)

@app.get("/threads/", dependencies=[Depends(api_key_auth)])
def _get_all_threads():
    res, set =cs.get_all_threads()
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

@app.get("/runners/", dependencies=[Depends(api_key_auth)])
def _get_all_runners() -> list[RunnerView]:
    res, set =cs.get_all_runners()
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

@app.get("/runners/{runner_id}", dependencies=[Depends(api_key_auth)])
def _get_runner(runner_id) -> RunnerView:
    res, set = cs.get_runner(runner_id)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=404, detail=f"No runner with id: {runner_id} a {set}")


@app.get("/stratins/", dependencies=[Depends(api_key_auth)])
def _get_all_stratins() -> list[StrategyInstance]:
    res, set =cs.get_all_stratins()
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

@app.post("/stratins/", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _create_stratin(new_stratin: StrategyInstance):
    res, id = cs.create_stratin(si=new_stratin)
    if res == 0: return id
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id}")
 
@app.patch("/stratins/{stratin_id}", dependencies=[Depends(api_key_auth)])
def _modify_stratin(stratin: StrategyInstance, stratin_id: UUID):
    if cs.is_stratin_running(id=stratin_id):
        res,id = cs.modify_stratin_running(si=stratin, id=stratin_id)
    else: 
        res, id = cs.modify_stratin(si=stratin, id=stratin_id)
    if res == 0: return id
    elif res == -2:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error not found: {res}:{id}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{id}")
 
@app.get("/stratins/{stratin_id}", dependencies=[Depends(api_key_auth)])
def _get_stratin(stratin_id) -> StrategyInstance:
    res, set = cs.get_stratin(stratin_id)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=404, detail=f"No stratin with id: {stratin_id} a {set}")

@app.put("/stratins/{stratin_id}/run", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _run_stratin(stratin_id: UUID, runReq: RunRequest):

    if runReq.mode == Mode.LIVE and get_environment() != Env.PROD:
                raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Live MODE disabled for TEST env")

    #print(runReq)
    if runReq.bt_from is not None and runReq.bt_from.tzinfo is None:
        runReq.bt_from = zoneNY.localize(runReq.bt_from)

    if runReq.bt_to is not None and runReq.bt_to.tzinfo is None:
        runReq.bt_to = zoneNY.localize(runReq.bt_to)  
    #pokud jedeme nad test intervaly anebo je požadováno více dní - pouštíme jako batch day by day
    #do budoucna dát na FE jako flag
    #print(runReq)
    if runReq.mode not in [Mode.LIVE, Mode.PAPER] and (runReq.test_batch_id is not None or (runReq.bt_from is not None and runReq.bt_to is not None and runReq.bt_from.date() != runReq.bt_to.date())):
        res, id = cs.run_batch_stratin(id=stratin_id, runReq=runReq)
    else:
        #not necessary for live/paper the weekdays are simply ignored, in the future maybe add validation if weekdays are presented
        #if runReq.weekdays_filter is not None:
        #        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Weekday only for backtest mode with batch (not single day)")
        res, id = cs.run_stratin(id=stratin_id, runReq=runReq)
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{id}")

@app.put("/runners/{runner_id}/pause", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _pause_runner(runner_id):
    res, id = cs.pause_runner(id=runner_id)
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")

@app.put("/runners/{runner_id}/stop", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _stop_runner(runner_id):
    res, id = cs.stop_runner(id=runner_id)
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")

@app.delete("/stratins/{stratin_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _delete_stratin(stratin_id):
    res, id = cs.delete_stratin(id=stratin_id)
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")

@app.put("/runners/stop", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def stop_all_runners():
    res, id = cs.stop_runner()
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")

@app.get("/tradehistory/{symbol}", dependencies=[Depends(api_key_auth)])
def get_trade_history(symbol: str, timestamp_from: float, timestamp_to:float) -> list[Trade]:
    res, set = cs.get_trade_history(symbol, timestamp_from, timestamp_to)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=404, detail=f"No trades found {res}")

@app.put("/migrate", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def migrate():
    lock_file = DATA_DIR + "/migr.lock"
    
    #if lock file not present, we can continue and create the file
    if not os.path.exists(lock_file):

        #migration code
        print("migration code STARTED")
        try:
            # Helper function to transform a row to a RunArchive object
            def row_to_object(row: dict) -> RunArchive:
                return RunArchive(
                    id=row.get('id'),
                    strat_id=row.get('strat_id'),
                    batch_id=row.get('batch_id'),
                    symbol=row.get('symbol'),
                    name=row.get('name'),
                    note=row.get('note'),
                    started=row.get('started'),
                    stopped=row.get('stopped'),
                    mode=row.get('mode'),
                    account=row.get('account'),
                    bt_from=row.get('bt_from'),
                    bt_to=row.get('bt_to'),
                    strat_json=row.get('strat_json'),
                    stratvars=row.get('stratvars'),
                    settings=row.get('settings'),
                    ilog_save=row.get('ilog_save'),
                    profit=row.get('profit'),
                    trade_count=row.get('trade_count'),
                    end_positions=row.get('end_positions'),
                    end_positions_avgp=row.get('end_positions_avgp'),
                    metrics=row.get('open_orders'),
                    #metrics=orjson.loads(row.get('metrics')) if row.get('metrics') else None,
                    stratvars_toml=row.get('stratvars_toml')
                )

            def get_all_archived_runners():
                conn = pool.get_connection()
                try:
                    conn.row_factory = lambda c, r: orjson.loads(r[0])
                    c = conn.cursor()
                    res = c.execute(f"SELECT data FROM runner_header")
                finally:
                    conn.row_factory = None
                    pool.release_connection(conn)        
                return 0, res.fetchall()

            set = list[RunArchive]

            def migrate_to_columns(ra: RunArchive):
                conn = pool.get_connection()
                try:

                    c = conn.cursor()
                    # statement = f"""UPDATE runner_header SET
                    #     strat_id='{str(ra.strat_id)}',
                    #     batch_id='{ra.batch_id}',
                    #     symbol='{ra.symbol}',
                    #     name='{ra.name}',
                    #     note='{ra.note}',
                    #     started='{ra.started}',
                    #     stopped='{ra.stopped}',
                    #     mode='{ra.mode}',
                    #     account='{ra.account}',
                    #     bt_from='{ra.bt_from}',
                    #     bt_to='{ra.bt_to}',
                    #     strat_json='ra.strat_json)',
                    #     settings='{ra.settings}',
                    #     ilog_save='{ra.ilog_save}',
                    #     profit='{ra.profit}',
                    #     trade_count='{ra.trade_count}',
                    #     end_positions='{ra.end_positions}',
                    #     end_positions_avgp='{ra.end_positions_avgp}',
                    #     metrics='{ra.metrics}',
                    #     stratvars_toml="{ra.stratvars_toml}"
                    # WHERE runner_id='{str(ra.strat_id)}'
                    # """
                    # print(statement)

                    res = c.execute('''
                        UPDATE runner_header 
                        SET strat_id=?, batch_id=?, symbol=?, name=?, note=?, started=?, stopped=?, mode=?, account=?, bt_from=?, bt_to=?, strat_json=?, settings=?, ilog_save=?, profit=?, trade_count=?, end_positions=?, end_positions_avgp=?, metrics=?, stratvars_toml=?
                        WHERE runner_id=?
                        ''',
                        (str(ra.strat_id), ra.batch_id, ra.symbol, ra.name, ra.note, ra.started, ra.stopped, ra.mode, ra.account, ra.bt_from, ra.bt_to, orjson.dumps(ra.strat_json).decode('utf-8'), orjson.dumps(ra.settings).decode('utf-8'), ra.ilog_save, ra.profit, ra.trade_count, ra.end_positions, ra.end_positions_avgp, orjson.dumps(ra.metrics).decode('utf-8'), ra.stratvars_toml, str(ra.id)))

                    conn.commit()
                finally:

                    pool.release_connection(conn)        
                return 0, res

            res, set = get_all_archived_runners()
            print(f"fetched {len(set)}")
            for row in set:
                ra: RunArchive = row_to_object(row)
                print(f"item {ra.id}")
                res, val = migrate_to_columns(ra)
                print(res,val)
                print("migrated", ra.id)




        finally:
            open(lock_file, 'w').close()

            return 0
             
        # res, set =cs.migrate_archived_runners()
        # if res == 0:
        #     open(lock_file, 'w').close()
        #     return set
        # else:
        #     raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")


    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Migration lock file present {lock_file}")


#ARCHIVE RUNNERS SECTION
# region Archive runners

#get all archived runners headers - just RunArchiveView
@app.get("/archived_runners/", dependencies=[Depends(api_key_auth)])
def _get_all_archived_runners() -> list[RunArchiveView]:
    res, set =cs.get_all_archived_runners()
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

#get all archived runners headers - just RunArchiveView - with pagination
@app.post("/archived_runners_p/", dependencies=[Depends(api_key_auth)])
def _get_all_archived_runners_p(req: DataTablesRequest) -> RunArchiveViewPagination:
    #print(req)
    #DataTablesRequest
    res, set =cs.get_all_archived_runners_p(req)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")


#get complete header data for specific archivedRunner = RunArchive
@app.get("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)])
def _get_archived_runner_header_byID(runner_id: UUID) -> RunArchive:
    res, set =cs.get_archived_runner_header_byID(runner_id)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

#delete archive runner from header and detail
@app.delete("/archived_runners/", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _delete_archived_runners_byIDs(runner_ids: list[UUID]):
    res, id = cs.delete_archived_runners_byIDs(ids=runner_ids)
    if res == 0: return id
    elif res < 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")

#get runners list based on batch_id
@app.get("/archived_runners/batch/{batch_id}", dependencies=[Depends(api_key_auth)])
def _get_archived_runnerslist_byBatchID(batch_id: str) -> list[UUID]:
    res, set =cs.get_archived_runnerslist_byBatchID(batch_id)
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")


#delete archive runner from header and detail
@app.delete("/archived_runners/batch/{batch_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _delete_archived_runners_byBatchID(batch_id: str):
    res, id = cs.delete_archived_runners_byBatchID(batch_id=batch_id)
    if res == 0: return id
    elif res == -1:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{batch_id}:{id}")


#WIP - TOM indicator preview from frontend f
#return indicator value for archived runner, return values list0 - bar indicators, list1 - ticks indicators
#TBD mozna predelat na dict pro prehlednost
@app.put("/archived_runners/{runner_id}/previewindicator", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _preview_indicator_byTOML(runner_id: UUID, indicator: InstantIndicator) -> list[dict]:
    #mozna pak pridat name
    res, vals = cs.preview_indicator_byTOML(id=runner_id, indicator=indicator)
    if res == 0: return vals
    elif res == -1:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{vals}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}")

#delete instant indicator from detail
@app.delete("/archived_runners/{runner_id}/previewindicator", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def _delete_indicator_byName(runner_id: UUID, indicator: InstantIndicator):
    #mozna pak pridat name
    res, vals = cs.delete_indicator_byName(id=runner_id, indicator=indicator)
    if res == 0: return vals
    elif res == -1:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{vals}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}")


#edit archived runner ("note",..)
@app.patch("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)])
def _edit_archived_runners(archChange: RunArchiveChange, runner_id: UUID):
    res, id = cs.edit_archived_runners(runner_id=runner_id, archChange=archChange)
    if res == 0: return runner_id
    elif res == -1:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error not found: {res}:{runner_id}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{id}")
    
#get all archived runners detail
@app.get("/archived_runners_detail/", dependencies=[Depends(api_key_auth)])
def _get_all_archived_runners_detail() -> list[RunArchiveDetail]:
    res, set =cs.get_all_archived_runners_detail()
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

#get archived runners detail by id
# @app.get("/archived_runners_detail/{runner_id}", dependencies=[Depends(api_key_auth)])
# def _get_archived_runner_details_byID(runner_id) -> RunArchiveDetail:
#     res, set = cs.get_archived_runner_details_byID(runner_id)
#     if res == 0:
#         return set
#     else:
#         raise HTTPException(status_code=404, detail=f"No runner with id: {runner_id} a {set}")

#this is the variant of above that skips parsing of json and returns JSON string returned from db
@app.get("/archived_runners_detail/{runner_id}", dependencies=[Depends(api_key_auth)])
def _get_archived_runner_details_byID(runner_id: UUID):
    res, data = cs.get_archived_runner_details_byID(id=runner_id, parsed=False)
    if res == 0:
        # Return the raw JSON string as a plain Response
        return Response(content=data, media_type="application/json")
    else:
        raise HTTPException(status_code=404, detail=f"No runner with id: {runner_id}. {data}")

#get archived runners detail by id
@app.get("/archived_runners_log/{runner_id}", dependencies=[Depends(api_key_auth)])
def _get_archived_runner_log_byID(runner_id: UUID, timestamp_from: float, timestamp_to: float) -> list[dict]:
    res = get_log_window(runner_id,timestamp_from, timestamp_to)
    if len(res) > 0:
        return res
    else:
        raise HTTPException(status_code=404, detail=f"No logs found with id: {runner_id} and between {timestamp_from} and {timestamp_to}")

def remove_ansi_codes(text):
    ansi_escape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
    return ansi_escape.sub('', text)

# endregion 
# A simple function to read the last lines of a file
# def tail(file_path, n=10, buffer_size=1024):
#     try:
#         with open(file_path, 'rb') as f:
#             f.seek(0, 2)  # Move to the end of the file
#             file_size = f.tell()
#             lines = []
#             buffer = bytearray()

#             for i in range(file_size // buffer_size + 1):
#                 read_start = max(-buffer_size * (i + 1), -file_size)
#                 f.seek(read_start, 2)
#                 read_size = min(buffer_size, file_size - buffer_size * i)
#                 buffer[0:0] = f.read(read_size)  # Prepend to buffer

#                 if buffer.count(b'\n') >= n + 1:
#                     break

#             lines = buffer.decode(errors='ignore').splitlines()[-n:]
#             lines = [remove_ansi_codes(line) for line in lines]
#             return lines
#     except Exception as e:
#         return [str(e) + format_exc()]

#updated version that reads lines line by line
def tail(file_path, n=10):
    try:
        with open(file_path, 'rb') as f:
            f.seek(0, 2)  # Move to the end of the file
            file_size = f.tell()
            lines = []
            line = b''

            f.seek(-1, 2)  # Start at the last byte
            while len(lines) < n and f.tell() != 0:
                byte = f.read(1)
                if byte == b'\n':
                    # Decode, remove ANSI codes, and append the line
                    lines.append(remove_ansi_codes(line.decode(errors='ignore')))
                    line = b''
                else:
                    line = byte + line
                f.seek(-2, 1)  # Move backwards by two bytes

            if line:
                # Append any remaining line after removing ANSI codes
                lines.append(remove_ansi_codes(line.decode(errors='ignore')))

            return lines[::-1]  # Reverse the list to get the lines in correct order
    except Exception as e:
        return [str(e)]


    
@app.get("/log", dependencies=[Depends(api_key_auth)])
def read_log(lines: int = 700, logfile: str = "strat.log"):
    log_path = LOG_PATH  / logfile
    return {"lines": tail(log_path, lines)}

#get alpaca history bars
@app.get("/history_bars/", dependencies=[Depends(api_key_auth)])
def _get_alpaca_history_bars(symbol: str, datetime_object_from: datetime, datetime_object_to: datetime, timeframe_amount: int, timeframe_unit: TimeFrameUnit) -> list[Bar]:
    print("Requested dates ",datetime_object_from,datetime_object_to)
    res, set =cs.get_alpaca_history_bars(symbol, datetime_object_from, datetime_object_to, TimeFrame(amount=timeframe_amount,unit=timeframe_unit))
    if res == 0:
        return set
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found {res} {set}")

#get pdf report - WIP
@app.put("/archived_runners/{runner_id}/generatepdf", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"application/pdf": {}}}})
def _generat_pdf(runner_id: UUID):
    #jako vstup umouznit i seznam runneru - vytvori to pote report ze vsech techto
    #pripadne mit jako vstup batch a udelat to pro batch ()
    res, vals = mt.create_trading_report_pdf(id=runner_id)
    if res == 0:
        # Return the PDF data as a streaming response {str(runner_id)}
        return StreamingResponse(vals, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=report.pdf"})
    elif res == -1:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error no runner: {runner_id} {res}:{vals}")
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}")

#generate image based list of ids
@app.post("/archived_runners/generatereportimage", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}})
def _generate_report_image(runner_ids: list[UUID]):
    try:
        res, stream = generate_trading_report_image(runner_ids=runner_ids,stream=True)
        if res == 0: return StreamingResponse(stream, media_type="image/png",headers={"Content-Disposition": "attachment; filename=report.png"})
        elif res < 0:
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{stream}")
    except Exception as e:
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {str(e)}" + format_exc())        


#TODO toto bude zaklad pro obecnou funkci, ktera bude volat ruzne analyzy
#vstupem bude obecny objekt, ktery ponese nazev analyzy + atributy
@app.post("/batches/optimizecutoff", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}})
def _optimize_cutoff(analyzerInputs: AnalyzerInputs):
    try:
        if len(analyzerInputs.runner_ids) == 0 and analyzerInputs.batch_id is None:
                     raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: batch_id or runner_ids required")   

        #bude predelano na obecny analyzator s obecnym rozhrannim
        res, stream = ci.find_optimal_cutoff.find_optimal_cutoff(runner_ids=analyzerInputs.runner_ids, batch_id=analyzerInputs.batch_id, stream=True, **analyzerInputs.params)
        if res == 0: return StreamingResponse(stream, media_type="image/png",headers={"Content-Disposition": "attachment; filename=optimizedcutoff.png"})
        elif res < 0:
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{id}")
    except Exception as e:
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {str(e)}" + format_exc())        

#obecna funkce pro analyzy
#vstupem bude obecny objekt, ktery ponese nazev analyzy + atributy
@app.post("/batches/analytics", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}})
def _generate_analysis(analyzerInputs: AnalyzerInputs):
    try:
        if (analyzerInputs.runner_ids is None or len(analyzerInputs.runner_ids) == 0) and analyzerInputs.batch_id is None:
                     raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: batch_id or runner_ids required")   

        funct = "ci."+analyzerInputs.function+"."+analyzerInputs.function
        custom_function = eval(funct)
        stream = None
        res, stream = custom_function(runner_ids=analyzerInputs.runner_ids, batch_id=analyzerInputs.batch_id, stream=True, **analyzerInputs.params)

        if res == 0: return StreamingResponse(stream, media_type="image/png")
        elif res < 0:
            print("Error when generating analysis: ",str(stream))
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{stream}")
    except Exception as e:
            raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {str(e)}" + format_exc())        

#TestList APIS - do budoucna predelat SQL do separatnich funkci
@app.post('/testlists/', dependencies=[Depends(api_key_auth)])
def create_record(testlist: TestList):
    # Generate a new UUID for the record
    testlist.id = str(uuid4())[:8]

    # Insert the record into the database
    conn = pool.get_connection()
    cursor = conn.cursor()
    cursor.execute("INSERT INTO test_list (id, name, dates) VALUES (?, ?, ?)", (testlist.id, testlist.name, orjson.dumps(testlist.dates, default=json_serial, option=orjson.OPT_PASSTHROUGH_DATETIME).decode('utf-8')))
    conn.commit()
    pool.release_connection(conn)
    return testlist


# API endpoint to retrieve all records
@app.get('/testlists/', dependencies=[Depends(api_key_auth)])
def get_testlists():
    res, sada = cs.get_testlists()
    if res == 0:
        return sada
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

# API endpoint to retrieve a single record by ID
@app.get('/testlists/{record_id}', dependencies=[Depends(api_key_auth)])
def get_testlist(record_id: str):
    res, testlist = cs.get_testlist_byID(record_id=record_id)

    if res == 0:
        return testlist
    elif res < 0:
        raise HTTPException(status_code=404, detail='Record not found')

# API endpoint to update a record
@app.put('/testlists/{record_id}', dependencies=[Depends(api_key_auth)])
def update_testlist(record_id: str, testlist: TestList):
    # Check if the record exists
    conn = pool.get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT id FROM test_list WHERE id = ?", (record_id,))
    row = cursor.fetchone()
    
    if row is None:
        raise HTTPException(status_code=404, detail='Record not found')
    
    # Update the record in the database
    cursor.execute("UPDATE test_list SET name = ?, dates = ? WHERE id = ?", (testlist.name, orjson.dumps(testlist.dates, default=json_serial, option=orjson.OPT_PASSTHROUGH_DATETIME).decode('utf-8'), record_id))
    conn.commit()
    pool.release_connection(conn)
    
    testlist.id = record_id
    return testlist

# API endpoint to delete a record
@app.delete('/testlists/{record_id}', dependencies=[Depends(api_key_auth)])
def delete_testlist(record_id: str):
    # Check if the record exists
    conn = pool.get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT id FROM test_list WHERE id = ?", (record_id,))
    row = cursor.fetchone()
    
    if row is None:
        raise HTTPException(status_code=404, detail='Record not found')
    
    # Delete the record from the database
    cursor.execute("DELETE FROM test_list WHERE id = ?", (record_id,))
    conn.commit()
    pool.release_connection(conn)
    
    return {'message': 'Record deleted'}

# region CONFIG APIS

# Get all config items
@app.get("/config-items/", dependencies=[Depends(api_key_auth)])
def get_all_items() -> list[ConfigItem]:
    res, sada = cf.get_all_config_items()
    if res == 0:
        return sada
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")


# Get a config item by ID
@app.get("/config-items/{item_id}", dependencies=[Depends(api_key_auth)])
def get_item(item_id: int)-> ConfigItem:
    res, sada = cf.get_config_item_by_id(item_id)
    if res == 0:
        return sada
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

# Get a config item by Name
@app.get("/config-items-by-name/", dependencies=[Depends(api_key_auth)])
def get_item(item_name: str)-> ConfigItem:
    res, sada = cf.get_config_item_by_name(item_name)
    if res == 0:
        return sada
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

# Create a new config item
@app.post("/config-items/", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def create_item(config_item: ConfigItem) -> ConfigItem:
    res, sada = cf.create_config_item(config_item)
    if res == 0: return sada
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id} {sada}")
 

# Update a config item by ID
@app.put("/config-items/{item_id}", dependencies=[Depends(api_key_auth)])
def update_item(item_id: int, config_item: ConfigItem) -> ConfigItem:
    res, sada = cf.get_config_item_by_id(item_id)
    if res != 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

    res, sada = cf.update_config_item(item_id, config_item)
    if res == 0: return sada
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id}")


# Delete a config item by ID
@app.delete("/config-items/{item_id}", dependencies=[Depends(api_key_auth)])
def delete_item(item_id: int) -> dict:
    res, sada = cf.get_config_item_by_id(item_id)
    if res != 0:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found")

    res, sada = cf.delete_config_item(item_id)
    if res == 0: return sada
    else:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id}")

# endregion

# region scheduler
# 1. Fetch All RunManagerRecords
@app.get("/run_manager_records/", dependencies=[Depends(api_key_auth)], response_model=List[RunManagerRecord])
#TODO zvazit rozsireni vystupu o strat_status (running/stopped)
def get_all_run_manager_records():
    result, records = rm.fetch_all_run_manager_records()
    if result != 0:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error fetching records")
    return records

# 2. Fetch RunManagerRecord by ID
@app.get("/run_manager_records/{record_id}", dependencies=[Depends(api_key_auth)], response_model=RunManagerRecord)
#TODO zvazit rozsireni vystupu o strat_status (running/stopped)
def get_run_manager_record(record_id: UUID):
    result, record = rm.fetch_run_manager_record_by_id(record_id)
    if result == -2:  # Record not found
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
    elif result != 0:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error fetching record")
    return record

# 3. Update RunManagerRecord 
@app.patch("/run_manager_records/{record_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def update_run_manager_record(record_id: UUID, update_data: RunManagerRecord):
    #make dates zone aware zoneNY
    # if update_data.valid_from is not None:
    #     update_data.valid_from = zoneNY.localize(update_data.valid_from)
    # if update_data.valid_to is not None:
    #     update_data.valid_to = zoneNY.localize(update_data.valid_to)
    result, message = rm.update_run_manager_record(record_id, update_data)
    if result == -2:  # Update failed
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) 
    elif result != 0:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error during update {result} {message}")
    return {"message": "Record updated successfully"} 

# 4. Delete RunManagerRecord
@app.delete("/run_manager_records/{record_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK)
def delete_run_manager_record(record_id: UUID):
    result, message = rm.delete_run_manager_record(record_id)
    if result == -2:  # Delete failed
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
    elif result != 0:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error during deletion {result} {message}")
    return {"message": "Record deleted successfully"} 

@app.post("/run_manager_records/", status_code=status.HTTP_201_CREATED)
def create_run_manager_record(new_record: RunManagerRecord, api_key_auth: Depends = Depends(api_key_auth)): 
    #make date zone aware - convert to zoneNY
    # if new_record.valid_from is not None:
    #     new_record.valid_from = zoneNY.localize(new_record.valid_from)
    # if new_record.valid_to is not None:
    #     new_record.valid_to = zoneNY.localize(new_record.valid_to)

    result, record_id = rm.add_run_manager_record(new_record)
    if result != 0:
        raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error during record creation: {result} {record_id}")
    return {"id": record_id}
# endregion

#model section
#UPLOAD MODEL
@app.post("/model/upload_model", dependencies=[Depends(api_key_auth)])
async def _upload_model(file: UploadFile = File(...)):
    # Specify the directory to save the file
    #save_directory = DATA_DIR+'/models/'
    save_directory = MODEL_DIR

    os.makedirs(save_directory, exist_ok=True)
    
    # Extract just the filename, discarding any path information
    base_filename = os.path.basename(file.filename)
    file_path = os.path.join(save_directory, base_filename)

    # Save the uploaded file
    with open(file_path, "wb") as buffer:
        while True:
            data = await file.read(1024)  # Read in chunks
            if not data:
                break
            buffer.write(data)

    print(f"saved to {file_path=} file:{base_filename=}")

    return {"filename": base_filename, "location": file_path}

#LIST MODELS
@app.get("/model/list-models", dependencies=[Depends(api_key_auth)])
def list_models():
    #models_directory = DATA_DIR + '/models/'
    models_directory = MODEL_DIR
    # Ensure the directory exists
    if not os.path.exists(models_directory):
        return {"error": "Models directory does not exist."}

    # List all files in the directory
    model_files = sorted(os.listdir(models_directory))
    return {"models": model_files}

@app.post("/model/upload-model", dependencies=[Depends(api_key_auth)])
def upload_model(file: UploadFile = File(...)):
    if not file:
        raise HTTPException(status_code=400, detail="No file uploaded.")
    file_location = os.path.join(MODEL_DIR, file.filename)
    with open(file_location, "wb+") as file_object:
        shutil.copyfileobj(file.file, file_object)

    return JSONResponse(status_code=200, content={"message": "Model uploaded successfully."})

@app.delete("/model/delete-model/{model_name}", dependencies=[Depends(api_key_auth)])
def delete_model(model_name: str):
    model_path = os.path.join(MODEL_DIR, model_name)
    if os.path.exists(model_path):
        os.remove(model_path)
        return {"message": "Model deleted successfully."}
    else:
        raise HTTPException(status_code=404, detail="Model not found.")

@app.get("/model/download-model/{model_name}", dependencies=[Depends(api_key_auth)])
def download_model(model_name: str):
    model_path = os.path.join(MODEL_DIR, model_name)
    if os.path.exists(model_path):
        return FileResponse(path=model_path, filename=model_name, media_type='application/octet-stream')
    else:
        raise HTTPException(status_code=404, detail="Model not found.")

@app.get("/model/metadata/{model_name}", dependencies=[Depends(api_key_auth)])
def get_metadata(model_name: str):
    try:
        #loadujeme pouze v modu cfg only
        model_instance = ml.load_model(file=model_name, directory=MODEL_DIR, cfg_only = True)
        try:
            metadata = model_instance.metadata
        except AttributeError:
            metadata = model_instance.__dict__
            del metadata["scalerX"]
            del metadata["scalerY"]
            del metadata["model"]
        except Exception as e:
            metadata = "No Metada" + str(e) + format_exc()
        return metadata
    except Exception as e:
        raise HTTPException(status_code=404, detail="Model not found."+str(e) + format_exc())
    
    # model_path = os.path.join(MODEL_DIR, model_name)
    # if os.path.exists(model_path):
    #     # Example: Retrieve metadata from a file or generate it
    #     metadata = {
    #         "name": model_name,
    #         "size": os.path.getsize(model_path),
    #         "last_modified": os.path.getmtime(model_path),
    #         # ... other metadata fields ...
    #     }


# Thread function to insert data from the queue into the database
def insert_queue2db():
    print("starting insert_queue2db thread")
    while True:
        # Retrieve data from the queue
        data = insert_queue.get()

        try:
            # Unpack the data
            runner_id, loglist = data
            c = insert_conn.cursor()
            insert_data = []
            for i in loglist:
                row = (str(runner_id), i["time"], orjson.dumps(i, default=json_serial, option=orjson.OPT_PASSTHROUGH_DATETIME|orjson.OPT_NON_STR_KEYS).decode('utf-8'))
                insert_data.append(row)
            c.executemany("INSERT INTO runner_logs VALUES (?,?,?)", insert_data)
            insert_conn.commit()
            # Mark the task as done in the queue
        except OperationalError as e:
            send_to_telegram("insert logs daemon returned" + str(e) + "RETRYING")
            if "database is locked" in str(e):
                # Database is locked, wait for a while and retry
                insert_queue.put(data)  # Put the data back into the queue for retry
                sleep(1)  # You can adjust the sleep duration
            else:
                raise  # If it's another error, raise it
        except Exception as e:
            print("ERROR INSERT LOGQUEUE MODULE:" + str(e)+format_exc())
            print(data)

#join cekej na dokonceni vsech
for i in cs.db.runners:
    i.run_thread.join()

if __name__ == "__main__":
    try:
        #TOTO predelat na samostatnou tridu typu vlakna a dat do separatniho souboru, draft jiz na chatgpt
        #spusteni vlakna pro zapis logů (mame single write vlakno, thready dodávají pres queue)
        insert_thread = Thread(target=insert_queue2db)
        insert_thread.start()

        #attach debugGER to be able to debug scheduler jobs (run in separate threads)
        # debugpy.listen(('localhost', 5678))
        # print("Waiting for debugger to attach...")
        # debugpy.wait_for_client()  # Script will pause here until debugger is attached

        #init scheduled tasks from schedule table
        #Add APS scheduler job refresh
        res, result = aps.initialize_jobs()
        if res < 0:
            #raise exception
            raise Exception(f"Error {res} initializing APS jobs, error {result}")

        uvicorn.run("__main__:app", host="0.0.0.0", port=8000, reload=False)
    except Exception as e:
        print("Error intializing app: " + str(e) + format_exc())
        aps.scheduler.shutdown(wait=False)
    finally:
        print("closing insert_conn connection")
        insert_conn.close()
        print("closed")


