#!/usr/bin/env python3

import urllib.request
import json

def read_user_agent(path_to_file):
    f = open(path_to_file, 'r')
    str_agent = f.read()
    f.close()
    return str_agent

def read_weather_data():
    """
    Tällä tavalla saadaan yr.no -sivustolta säätiedotuksia jsonina pihalle.
    """
    # Greenwich
    # request_url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=51.5&lon=0'
    # Oulu
    request_url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=65.0&lon=25.5'
    # User agent pitää olla, jotta YR antaa dataa, oli joku nettisivu, joka näytti tämän
    user_agent = read_user_agent('./user_agent.txt')
    headers = {'User-Agent': user_agent,}
    # Data saadaan ulos näin
    response = urllib.request.Request(request_url, None, headers)
    req = urllib.request.urlopen(response)
    string_data = req.read()
    json_data = json.loads(string_data)
    return json_data

def find_location():
    """
    Returns a location data for forecast as a list. Location has form 'longitude, latitude, elevation'.
    """
    json_data = read_weather_data()
    location = json_data['geometry']['coordinates']
    return location

def parse_timeseries():
    """
    Parses json weather data to dictionary in form {date: [(time_0, air_temperature), (time_1, air_temperature), ... ] }
    """
    # json_data = read_weather_data()

    # Vähennetään netin yli tapahtuvaa tiedonsiirtoa ja luetaan data tiedostosta
    f = open('./data.json', 'r')
    json_data = json.loads(f.read())
    f.close()

    timeseries = json_data['properties']['timeseries']

    weather_data = {} # a dictionary to store data as a {date: [time]}

    for t in timeseries:
        air_temperature = t['data']['instant']['details']['air_temperature']
        date, time = t['time'].strip('Z').split('T')
        if date not in weather_data.keys():
            weather_data[date] = [(time, air_temperature)]
        else:
            weather_data[date].append((time, air_temperature))

    return weather_data

def parse_time(time):
    """
    time: str 'hh:mm:ss'
    return: str 'hh:mm'
    """
    timelist = time.split(':')
    return timelist[0] + ':' + timelist[1]

def parse_date(date):
    """
    date: str 'yyyy-mm-dd'
    return: str 'dd.mm.yyyy'
    """
    datelist = date.split('-')
    return datelist[2] + '.' + datelist[1] + '.' + datelist[0]

def print_weather_data():
    data = parse_timeseries()
    for key, value in data.items():
        print(parse_date(key))
        print('----------')
        print('time\t', 'temp (C)')
        for v in value:
            print(parse_time(v[0]), '\t', v[1])

        print()

if __name__ == '__main__':
    # find_location()
    # read_weather_data()
    print_weather_data()


import sqlite3
from sqlite3 import Error
import os
import pandas as pd
from ben_gislason_cse_exercise.utilities.db_read import dbRead
from ben_gislason_cse_exercise.utilities.db_open import openDb
from ben_gislason_cse_exercise.utilities.db_write import dbWrite

class TestRW:
    def __init__(self, db_path):
        """
        Open connection to database for downstream use
        """
        #Set path of local copy of database
        self.path = db_path
        #Open a connection to local copy

        try:
            self.conn = sqlite3.connect(self.path)
        except Error as e:
            print(e)

        print(str(os.getcwd()).replace("\\","/")+"/tests/test.db")

    def run_test(self):
        """
        Create dataframes and generate plot and
        summary statistical table in copy of
        database
        """

        #Set data frames to add to test_table
        columns = ['test1','test2']
        df_1 = pd.DataFrame({columns[0]:1
                             , columns[1]:1
                             },index = [0])
        df_2 = pd.DataFrame({columns[0]:2
                             , columns[1]:2
                             },index = [1])

        df_test = pd.DataFrame({columns[0]:3
                             , columns[1]:3
                             },index = [0])

        df_out = df_1.append(df_2)
        print("Columns to sum: ")
        print(df_out)

        #Set Summation results
        sum1 = round(df_out[columns[0]].sum(),2)
        sum2 = round(df_out[columns[1]].sum(),2)

        df_sum = pd.DataFrame({columns[0]:sum1
                             , columns[1]:sum2
                             }, index = [0])
        print(df_sum)
        df_out = df_out.append(df_sum)
        print(df_out)

        dbTable = "test_table"

        #Creating a cursor object using the cursor() method
        cursor = self.conn.cursor()

        #Dropping test table if already exists
        cursor.execute("DROP TABLE test_table")

        df_out.to_sql(dbTable, self.conn, if_exists='fail')

        df_in = dbRead(self.conn, dbTable)
        self.conn.close()

        #Print results
        print("Table with summation read from db file")
        print(df_in)

        print("Test Values: ")
        print(df_test)

        if df_in.iloc[2][1] == df_test.iloc[0][0] and df_in.iloc[2][2] == df_test.iloc[0][1]:
            print("TestRW status: Pass")
        else:
            print("TestRW status: Fail")

tO = TestRW(str(os.getcwd()).replace("\\","/")+"/test.db")
TestRW.run_test(tO)

Data Anlaysis and Visualizations with Python and Matplotlib/data2info/data2info/tests/test_db_RW.py


#! /usr/bin/env python3

# Adapted from http://kitchingroup.cheme.cmu.edu/blog/2013/02/18/Nonlinear-curve-fitting/

import numpy as np  # type: ignore
import pandas as pd  # type: ignore
from scipy.optimize import leastsq  # type: ignore
import argparse
import warnings

from pymatgen.io.vasp import Vasprun  # type: ignore
from pymatgen.io.vasp.outputs import UnconvergedVASPWarning  # type: ignore

import matplotlib  # type: ignore
import matplotlib.pyplot as plt  # type: ignore

from vasppy.poscar import Poscar
from vasppy.summary import find_vasp_calculations
from vasppy.utils import match_filename

warnings.filterwarnings("ignore", category=UserWarning, module="pymatgen")
matplotlib.use("agg")

def parse_args():
    parser = argparse.ArgumentParser(
        description="Perform a Murnaghan equation of state fit across VASP subdirectories"
    )
    parser.add_argument(
        "-p", "--plot", action="store_true", help="generate murn.pdf plot of fit"
    )
    parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
    args = parser.parse_args()
    return args

def read_vasprun(filename):
    return Vasprun(
        filename, parse_potcar_file=False, parse_dos=False, parse_eigen=False
    )

def read_data(verbose=True):
    dir_list = find_vasp_calculations()
    if not dir_list:
        raise ValueError(
            "Did not find any subdirectories containing vasprun.xml or vasprun.xml.gz files"
        )
    data = []
    for d in dir_list:
        converged = True
        try:
            with warnings.catch_warnings(record=True) as w:
                vasprun = read_vasprun(match_filename(d + "vasprun.xml"))
                for warning in w:
                    if isinstance(warning.message, UnconvergedVASPWarning):
                        converged = False
                    else:
                        print(warning.message)
        except Exception:
            continue
        poscar = Poscar.from_file(d + "POSCAR")
        data.append(
            [
                poscar.scaling,
                vasprun.final_structure.volume,
                vasprun.final_energy,
                converged,
            ]
        )
    column_titles = ["scaling", "volume", "energy", "converged"]
    df = pd.DataFrame(data, columns=column_titles).sort_values(by="scaling")
    df = df.reset_index(drop=True)
    df["scaling_factor"] = df.volume / df.scaling**3
    scaling_factor_round = 4
    if verbose:
        print(df.to_string(index=False))
    if len(set(df.scaling_factor.round(scaling_factor_round))) != 1:
        raise ValueError("POSCAR scaling factors and volumes are inconsistent")
    return df

def murnaghan(vol, e0, b0, bp, v0):
    """
    Calculate the energy as a function of volume, using the Murnaghan equation of state
    [Murnaghan, Proc. Nat. Acad. Sci. 30, 244 (1944)]
    https://en.wikipedia.org/wiki/Murnaghan_equation_of_state
    cf. Fu and Ho, Phys. Rev. B 28, 5480 (1983).

    Args:
        vol (float): this volume.
        e0 (float):  energy at the minimum-energy volume, E0.
        b0 (float):  bulk modulus at the minimum-energy volume, B0.
        bp (float):  pressure-derivative of the bulk modulus at the minimum-energy volume, B0'.
        v0 (float):  volume at the minimum-energy volume, V0.

    Returns:
        (float): The energy at this volume.
    """
    energy = (
        e0 + b0 * vol / bp * (((v0 / vol) ** bp) / (bp - 1) + 1) - v0 * b0 / (bp - 1.0)
    )
    return energy

def objective(pars, x, y):
    err = y - murnaghan(x, *pars)
    return err

def lstsq_fit(volumes, energies):
    e_min = energies.min()
    v_min = volumes[np.argwhere(energies == e_min)[0][0]]
    x0 = [e_min, 2.0, 10.0, v_min]  # initial guess of parameters
    plsq = leastsq(objective, x0, args=(volumes, energies))
    return plsq

def make_plot(df, fit_params):
    v_min = df.volume.min() * 0.99
    v_max = df.volume.max() * 1.01
    v_fitting = np.linspace(v_min, v_max, num=50)
    e_fitting = murnaghan(v_fitting, *fit_params)
    plt.figure(figsize=(8.0, 6.0))
    # plot converged data points
    loc = df.converged
    plt.plot(df[loc].volume, df[loc].energy, "o")
    # plot unconverged data points
    loc = [not b for b in df.converged]
    plt.plot(df[loc].volume, df[loc].energy, "o", c="grey")
    # plot fitted equation of state curve
    plt.plot(v_fitting, e_fitting, "--")
    plt.xlabel(r"volume [$\mathrm{\AA}^3$]")
    plt.ylabel(r"energy [eV]")
    plt.tight_layout()
    plt.savefig("murn.pdf")

def fit(verbose=False, plot=False):
    df = read_data(verbose=verbose)
    e0, b0, bp, v0 = lstsq_fit(np.array(df.volume), np.array(df.energy))[0]
    if plot:
        make_plot(df, (e0, b0, bp, v0))
    print("E0: {:.4f}".format(e0))
    print("V0: {:.4f}".format(v0))
    print("opt. scaling: {:.5f}".format((v0 / df.scaling_factor.mean()) ** (1 / 3)))

def main():
    args = parse_args()
    fit(verbose=args.verbose, plot=args.plot)

if __name__ == "__main__":
    main()


import pygame
import random

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
PINK = (255, 0, 221)
YELLOW = (255, 228, 0)

# set values
game_name = 'DCCP Snake Game 2016-10621 김무승'
row_num = 80
col_num = 60
food_num = 20
block_size = 10
tick_val = 10
speed_var = 10
player1_initial_point = (60, 30)
player2_initial_point = (20, 30)
background_color = BLACK
player1_headcolor = RED
player1_bodycolor = BLUE
player2_headcolor = PINK
player2_bodycolor = YELLOW
food_color = GREEN

pygame.init()

count1 = 0
count2 = 0
display_size = (row_num * block_size , col_num * block_size)
game_display = pygame.display.set_mode(display_size)
pygame.display.set_caption(game_name)
clock = pygame.time.Clock()
x_pos_his = []
y_pos_his = []
x2_pos_his = []
y2_pos_his = []

game_over = False

class Player:
    dx = 0
    dy = 0

    def __init__(self, display, x, y):
        self.x = x
        self.y = y
        self.display = display

    def handle_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                if self.dx != speed_var:
                    self.dx = -speed_var
                    self.dy = 0
            elif event.key == pygame.K_RIGHT:
                if self.dx != -speed_var:
                    self.dx = speed_var
                    self.dy = 0
            elif event.key == pygame.K_UP:
                if self.dy != speed_var:
                    self.dx = 0
                    self.dy = -speed_var
            elif event.key == pygame.K_DOWN:
                if self.dy != -speed_var:
                    self.dx = 0
                    self.dy = speed_var

    def tick(self):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RSHIFT:
                self.x += 2*self.dx
                self.y += 2*self.dy
            else:
                self.x += self.dx
                self.y += self.dy
        else:
            self.x += self.dx
            self.y += self.dy

    def draw(self):
        pygame.draw.rect(self.display, player1_headcolor, [self.x, self.y, block_size, block_size])

    def drawbody(self):
        pygame.draw.rect(self.display, player1_bodycolor, [x_pos_his[-(i+2)], y_pos_his[-(i+2)], block_size, block_size])

    def handle_event2(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_a:
                if self.dx != speed_var:
                    self.dx = -speed_var
                    self.dy = 0
            elif event.key == pygame.K_d:
                if self.dx != -speed_var:
                    self.dx = speed_var
                    self.dy = 0
            elif event.key == pygame.K_w:
                if self.dy != speed_var:
                    self.dx = 0
                    self.dy = -speed_var
            elif event.key == pygame.K_s:
                if self.dy != -speed_var:
                    self.dx = 0
                    self.dy = speed_var

    def tick2(self):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LSHIFT:
                self.x += 2*self.dx
                self.y += 2*self.dy
            else:
                self.x += self.dx
                self.y += self.dy
        else:
            self.x += self.dx
            self.y += self.dy

    def draw2(self):
        pygame.draw.rect(self.display, player2_headcolor, [self.x, self.y, block_size, block_size])

    def drawbody2(self):
        pygame.draw.rect(self.display, player2_bodycolor, [x2_pos_his[-(i+2)], y2_pos_his[-(i+2)], block_size, block_size])

class Food:
    active = True
    def __init__(self, display):
        self.display = display
        self.x = random.randint(0,(display_size[0]/10)-1) * block_size
        self.y = random.randint(0,(display_size[1]/10)-1) * block_size

    def draw(self):
        pygame.draw.rect(self.display, food_color, [self.x, self.y, block_size, block_size])

player = Player(game_display, player1_initial_point[0] * block_size, player1_initial_point[1] * block_size)
player2 = Player(game_display, player2_initial_point[0] * block_size, player2_initial_point[1] * block_size)
foods = [Food(game_display) for _ in range(food_num)]

food_active = [True for _ in range(food_num)]

while not game_over:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            game_over = True
            break
        player.handle_event(event)
        player2.handle_event2(event)

    player.tick()
    player2.tick2()

    game_display.fill((background_color))
    player.draw()
    player2.draw2()
    x_pos_his.append(player.x)
    y_pos_his.append(player.y)
    x2_pos_his.append(player2.x)
    y2_pos_his.append(player2.y)

    if foods[count1 + count2].active:
        foods[count1 + count2].draw()

    if player.x == foods[count1 + count2].x and player.y == foods[count1 + count2].y:
        foods[count1 + count2].active = False
        count1 += 1

    if player2.x == foods[count1 + count2].x and player2.y == foods[count1 + count2].y:
        foods[count1 + count2].active = False
        count2 += 1

    if count1 > 0:
        for i in range(count1):
            player.drawbody()
            if (x_pos_his[-(i+2)] == player.x) and (y_pos_his[-(i+2)] == player.y):
                game_over = True
            elif (x_pos_his[-(i+2)] == player2.x) and (y_pos_his[-(i+2)] == player2.y):
                game_over = True

    if count2 > 0:
        for i in range(count2):
            player.drawbody2()
            if (x2_pos_his[-(i+2)] == player2.x) and (y2_pos_his[-(i+2)] == player2.y):
                game_over = True
            elif (x2_pos_his[-(i+2)] == player.x) and (y2_pos_his[-(i+2)] == player.y):
                game_over = True

    pygame.display.update()

    food_remains = False

    for food in foods:
        if food.active:
            food_remains = True

    if not food_remains:
        game_over = True

    if (player.x < 0) or (player.x > display_size[0]) or (player.y < 0) or (player.y > display_size[1]):
        game_over = True

    if (player2.x < 0) or (player2.x > display_size[0]) or (player2.y < 0) or (player2.y > display_size[1]):
        game_over = True

    clock.tick(tick_val)


#Added animation code to Aida's example_python.py

#modules for animation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import math
from collections import defaultdict
import matplotlib.cm as cm
import sys
import os
import matplotlib.animation as animation
plt.rcParams['animation.ffmpeg_path'] ="C:/ffmpeg/bin/ffmpeg.exe"

# the pyd module is in a subdir, add here for convenience
from sys import version_info, path
path.insert(0, './build/Debug')

from datetime import datetime
from sys import version_info

if (version_info.major < 3):
    if (version_info.minor < 11):
        if (version_info.micro < 4):
            raise Exception("The binary requires at least python version 3.11.4")

# Hello kinoshita! Good luck :)

#Name of case and saved animation
MODE="Pressure"
title=MODE+".mp4"

# I made it all into one module! ^-^
import komurasakiairbreathing as ka

# Some values that are re-used.
length_of_tube = 0.5
height_of_tube = 0.028

# The main object that defines the 'simulation case'.
# Simcase(total simulation time, dt)
simcase = ka.SimCase()
simcase.dt = 10.0E-6
simcase.simulation_duration = 10.0E-4
nstep=int(simcase.simulation_duration/simcase.dt) #todo: A: add simple callback method in python library to get simcase.n_time_step

solver_settings = ka.SolverSettings() # for now, use the default solver settings. See cpp file for default values.
chapman_jouget = ka.ChapmanJougetInitialConditionParameters()
chapman_jouget.beam_power = 2000E3 # Watt
chapman_jouget.energy_absorption_coefficient = 1

# Mesh Spacing objects describe how he grid is laid out over the domain in one specific axis. Right now, only MeshSpacingType.constant is defined, and the others will probably crash the program.
# ka.MeshSpacing(
#   ka.MeshSpacingType,
#   length of the axis. Should be equal to the size that will be defined in AddDoman,
#   total amount of cells that will be distributed
#   characteristic density left (leave 0 for constant)
#   characteristic density right (leave 0 for constant)
# )

x_mesh_spacing = ka.MeshSpacing()
x_mesh_spacing.type = ka.MeshSpacingType.constant
x_mesh_spacing.amount_of_elements = 250
y_mesh_spacing = ka.MeshSpacing()
y_mesh_spacing.type = ka.MeshSpacingType.constant
y_mesh_spacing.amount_of_elements = 14

# A 2d, rectangular domain where the flow will be simulated
tube = simcase.AddDomain(
    1,                                                      # ID
    "Tube",                                                 # Name, used for outputting
    ka.Position(0,0),                                       # Where the bottom-left corner of this domain is placed
    (length_of_tube, height_of_tube),                       # The x,y dimensions of this domain
    (x_mesh_spacing, y_mesh_spacing),                       # Mesh spacing parameters; how the grid is distributed
    ka.InitialisationMethod.from_chapman_jouget_solution,   # How this domain is initialised
)

# Setting the boundaries for the domain. Should be pretty self-explanatory.
tube.SetBoundaryType(ka.Face.left, ka.BoundaryCondition.slip)
tube.SetBoundaryType(ka.Face.top, ka.BoundaryCondition.slip)
tube.SetBoundaryType(ka.Face.bottom, ka.BoundaryCondition.slip)

ambient = simcase.AddDomain(
    2,
    "Ambient",
    ka.Position(length_of_tube,0),
    (length_of_tube, height_of_tube),
    (x_mesh_spacing, y_mesh_spacing),
    ka.InitialisationMethod.ambient_conditions,
)

ambient.SetBoundaryType(ka.Face.right, ka.BoundaryCondition.slip)
ambient.SetBoundaryType(ka.Face.top, ka.BoundaryCondition.slip)
ambient.SetBoundaryType(ka.Face.bottom, ka.BoundaryCondition.slip)

# the tube and the ambient share a boundary. This connects them.
simcase.ConnectBoundariesByName("Tube", ka.Face.right, "Ambient", ka.Face.left)

# Sets what we're interested in. After running the simulation, we can access these records.
recordName1="Tube_"+MODE
recordName2="Ambient_"+MODE
if MODE=="Density":
    simcase.AddRecord(tube.density.current_time_step,recordName1)
    simcase.AddRecord(ambient.density.current_time_step, recordName2)
    CBrabel="Density []"
elif MODE=="Temperature":
    simcase.AddRecord(tube.temperature.current_time_step,recordName1)
    simcase.AddRecord(ambient.temperature.current_time_step, recordName2)
    CBrabel="Temperature []"
elif MODE=="Pressure":
    simcase.AddRecord(tube.pressure.current_time_step,recordName1)
    simcase.AddRecord(ambient.pressure.current_time_step, recordName2)
    CBrabel="Presuure [Pa]"

# 数値魔法
start_time = datetime.now()
ka.DoSimulation(simcase)
end_time = datetime.now()

print("Simulation done! time ran: ", end_time - start_time)

#Animation
print("Nstep=",nstep)

#Get the range of variable in order to set the range of colorbar
var_min=0
var_max=0
for i in range(nstep):
    a=simcase.two_dimensional_array_records[recordName1].AsNumpyArray(i)
    b=simcase.two_dimensional_array_records[recordName2].AsNumpyArray(i)
    if var_min == 0:
        var_min = min(a.min(),b.min())
    else:
        if var_min > min(a.min(),b.min()):
            var_min = min(a.min(),b.min())
        if var_max < max(a.max(),b.max()):
            var_max = max(a.max(),b.max())

def animate(nt):
    fig.clear()
    #Create directory to store images
    exportPath = r'IMAGES'
    if not os.path.isdir(exportPath):
        os.makedirs(exportPath)

    #Display field in background
    a=simcase.two_dimensional_array_records[recordName1].AsNumpyArray(nt)
    b=simcase.two_dimensional_array_records[recordName2].AsNumpyArray(nt)
    xarr1=np.linspace(0,length_of_tube,x_mesh_spacing.amount_of_elements)
    xarr2=np.linspace(length_of_tube,length_of_tube*2,x_mesh_spacing.amount_of_elements)
    yarr=np.linspace(0,height_of_tube,y_mesh_spacing.amount_of_elements)
    plt.pcolormesh(xarr1,yarr,a,cmap='jet',vmin=var_min,vmax=var_max)
    plt.pcolormesh(xarr2,yarr,b,cmap='jet',vmin=var_min,vmax=var_max)

    cb = plt.colorbar(fraction=0.05,aspect=40,orientation="horizontal")
    cb.ax.tick_params(labelsize=20)
    cb.set_label(CBrabel,fontweight='bold',fontsize=25)
	# cb.ax.xaxis.set_ticks_position('top')

    # Plot options
    fig.set_tight_layout(True)
    # plt.xlim(xmin*1000,xmax*1000)
    # plt.ylim(ymin*1000,ymax*1000)
    plt.xticks(color='k',size=20,fontweight='bold')
    plt.yticks(color='k',size=20,fontweight='bold')
    plt.gca().set_aspect('equal', adjustable='box')
    plt.title(MODE+str(round(nt*simcase.dt*10**3,2))+' ms.',fontsize=35,fontweight='bold')
    plt.xlabel('Axial Direction (m)',fontsize=20,fontweight='bold')
    plt.ylabel('Radial Direction (m)',fontsize=20,fontweight='bold')

    # Show rocket walls
    wallthickness=0.002
    rectangle_x = [0,0,length_of_tube,length_of_tube]
    rectangle_y = [0,-wallthickness,-wallthickness,0]
    triangle_x = [-0.02,0,0]
    triangle_y = [0,height_of_tube+wallthickness,0]
    plt.fill([x for x in rectangle_x],[x for x in rectangle_y],'0.75',linewidth=0)
    plt.fill([x for x in triangle_x],[x for x in triangle_y],'0.75',linewidth=0)

    plt.draw()

    return
    #End of animate function

# EXPORT ANIMATION
fig, axes = plt.subplots(num=None, figsize=(15, 8), dpi=200, facecolor='w', edgecolor='k')
fig.clear()
plt.axis('equal')

anim=animation.FuncAnimation(fig,animate,range(nstep))
FFMpegWriter=animation.writers['ffmpeg']
writer=FFMpegWriter(fps=5, bitrate=100)
anim.save(title,writer=writer)

pause = input()


import hmac
import shlex
import subprocess
import time
from datetime import datetime
from pathlib import Path
import json
from urllib.parse import urljoin

import requests
from hashlib import sha256
from tempfile import TemporaryDirectory

from fastapi import FastAPI, HTTPException, Request
from github import Github

from .config import load_config
from .constants import (
    GITHUB_WEBHOOK_SECRET,
    GITHUB_ACCESS_TOKEN,
    MAX_DOWNLOAD_ARTIFACT_RETRIES,
)
from .data import load_build_data, Target as DataTarget, save_build_data

app = FastAPI()
config = load_config()
data = load_build_data()
github = Github(GITHUB_ACCESS_TOKEN)

async def verify_github_webhook(request: Request):
    # Take sha256 hmac of the request body, using GITHUB_WEBHOOK_TOKEN as the key
    if not GITHUB_WEBHOOK_SECRET:
        raise HTTPException(status_code=500, detail="GITHUB_WEBHOOK_TOKEN not set")
    hmac_sha256 = hmac.new(
        GITHUB_WEBHOOK_SECRET.encode(), await request.body(), sha256
    ).hexdigest()
    if not request.headers.get("X-Hub-Signature-256") == f"sha256={hmac_sha256}":
        raise HTTPException(status_code=401, detail="Unauthorized")

def github_get_request(url):
    response = requests.get(
        url,
        headers={
            "Authorization": f"token {GITHUB_ACCESS_TOKEN}",
            "Accept": "application/vnd.github.v3+json",
        },
    )
    response.raise_for_status()
    return response.json()

def reload_controller(target):
    # Update the controller with the /reload endpoint
    reload_response = requests.get(urljoin(target.controller_api_url, "reload"))
    reload_response.raise_for_status()

def check_controller_connection(target):
    # This is a very simple stopgap to prevent a corrupted state when a target
    # machine is not running; all this is meant to do is raise an error when
    # things don't work
    current_experience_response = requests.get(
        urljoin(target.controller_api_url, "current")
    )
    current_experience_response.raise_for_status()

def set_commit_status(repository_name, sha, state, context, description=None):
    github.get_repo(repository_name).get_commit(sha).create_status(
        state, context=f"footron-ci/{context}", description=description
    )

def ssh_production_option():
    return f'-o "SetEnv=FT_PRODUCTION={datetime.now().strftime("%d%H")}"'

def rsync_ssh_production_command():
    return f"ssh {ssh_production_option()}"

def handle_workflow_run_completed(event):
    workflow_run = event["workflow_run"]

    if workflow_run["status"] != "completed" or workflow_run["event"] != "push":
        return

    branch = workflow_run["head_branch"]
    sha = workflow_run["head_sha"]

    try:
        target = config.targets[branch]
    except KeyError:
        return

    event_name = workflow_run["name"]
    artifacts_url = workflow_run["artifacts_url"]

    repository_name = event["repository"]["full_name"]

    if event_name not in ["build-controls", "build-experiences"]:
        raise HTTPException(status_code=400, detail="Unknown event name")

    try:
        check_controller_connection(target)
    except requests.RequestException:
        set_commit_status(
            repository_name, sha, "failure", event_name, "Failed to connect"
        )
        raise

    # Start timer
    start_time = time.time()

    if event_name == "build-controls":
        # Request the artifacts endpoint using access token
        artifacts_response = github_get_request(artifacts_url)

        artifacts = artifacts_response["artifacts"]

        experiences_build_url = None
        for artifact in artifacts:
            if artifact["name"] == "web-build":
                experiences_build_url = artifact["archive_download_url"]
                break
        else:
            raise RuntimeError("Missing web build")

        # Download the artifact
        with TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            web_build_zip_path = temp_path / "web-build.zip"
            set_commit_status(
                repository_name, sha, "pending", event_name, "Downloading artifacts"
            )
            download_artifact_retries = 0
            while True:
                try:
                    subprocess.run(
                        [
                            "curl",
                            "-H",
                            f"Authorization: token {GITHUB_ACCESS_TOKEN}",
                            "-L",
                            experiences_build_url,
                            "-o",
                            str(web_build_zip_path),
                        ]
                    )
                except subprocess.CalledProcessError:
                    if download_artifact_retries < MAX_DOWNLOAD_ARTIFACT_RETRIES:
                        download_artifact_retries += 1
                        print(
                            f"Failed to download artifact; retrying in 1 second... ({download_artifact_retries}/{MAX_DOWNLOAD_ARTIFACT_RETRIES})"
                        )
                        time.sleep(1)
                        continue
                    set_commit_status(
                        repository_name,
                        sha,
                        "failure",
                        event_name,
                        "Failed to download controls artifact",
                    )
                    raise
                break

            set_commit_status(
                repository_name, sha, "pending", event_name, "Extracting artifacts"
            )
            # Extract the artifacts
            subprocess.run(
                ["unzip", str(web_build_zip_path), "-d", str(temp_path / "build")]
            )

            # rsync the build directory to the target directory
            set_commit_status(
                repository_name, sha, "pending", event_name, "Copying web build"
            )
            print(f'rsync {temp_path / "build"}/ {target.web_path}')
            subprocess.run(
                [
                    "rsync",
                    "-e",
                    rsync_ssh_production_command(),
                    "-a",
                    "--delete",
                    f'{str(temp_path / "build")}/',
                    str(target.web_path),
                ]
            )

            set_commit_status(
                repository_name, sha, "pending", event_name, "Reloading controller"
            )
            reload_controller(target)
    elif event_name == "build-experiences":
        artifacts_response = github_get_request(artifacts_url)

        artifacts = artifacts_response["artifacts"]

        for artifact in artifacts:
            if artifact["name"] == "experiences":
                experiences_build_url = artifact["archive_download_url"]
                break
        else:
            set_commit_status(
                repository_name,
                sha,
                "failure",
                event_name,
                "Missing experiences build",
            )
            raise RuntimeError("Missing experiences build")

        # Download the artifact using the access token in a curl header
        with TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            experience_build_zip_path = temp_path / "experiences.zip"

            set_commit_status(
                repository_name, sha, "pending", event_name, "Downloading artifacts"
            )
            download_artifact_retries = 0
            while True:
                try:
                    subprocess.run(
                        [
                            "curl",
                            "-H",
                            f"Authorization: token {GITHUB_ACCESS_TOKEN}",
                            "-L",
                            experiences_build_url,
                            "-o",
                            str(experience_build_zip_path),
                        ]
                    ).check_returncode()
                except subprocess.CalledProcessError:
                    if download_artifact_retries < MAX_DOWNLOAD_ARTIFACT_RETRIES:
                        download_artifact_retries += 1
                        print(
                            f"Failed to download artifact; retrying in 1 second... ({download_artifact_retries}/{MAX_DOWNLOAD_ARTIFACT_RETRIES})"
                        )
                        time.sleep(1)
                        continue
                    set_commit_status(
                        repository_name,
                        sha,
                        "failure",
                        event_name,
                        "Failed to download expereinces artifact",
                    )
                    raise
                break

            # Extract the artifacts
            set_commit_status(
                repository_name, sha, "pending", event_name, "Extracting artifacts"
            )

            try:
                subprocess.run(
                    ["unzip", str(experience_build_zip_path), "-d", temp_path]
                ).check_returncode()
            except subprocess.CalledProcessError:
                set_commit_status(
                    repository_name,
                    sha,
                    "failure",
                    event_name,
                    "Failed to unzip web build",
                )
                raise

            set_commit_status(
                repository_name, sha, "pending", event_name, "Comparing hashes"
            )
            hashes_path = temp_path / "hashes.json"
            with open(hashes_path, "r") as hashes_file:
                hashes = json.load(hashes_file)
            if branch not in data.targets:
                data.targets[branch] = DataTarget()

            existing_hashes = data.targets[branch].hashes
            hashes_keys = set(hashes.keys())
            existing_hashes_keys = set(existing_hashes.keys())
            new_hashes = hashes_keys - existing_hashes_keys
            deleted_hashes = existing_hashes_keys - hashes_keys
            overlapping_hashes = hashes_keys & existing_hashes_keys
            changed_hashes = set()

            for hash_key in new_hashes:
                data.targets[branch].hashes[hash_key] = hashes[hash_key]
            for hash_key in overlapping_hashes:
                if hashes[hash_key] != existing_hashes[hash_key]:
                    changed_hashes.add(hash_key)

            controller_host = target.controller_host
            controller_fs_path = target.controller_fs_path

            print("Added experiences:", list(new_hashes))
            print("Deleted experiences:", list(deleted_hashes))
            print("Changed experiences:", list(changed_hashes))

            set_commit_status(
                repository_name,
                sha,
                "pending",
                event_name,
                "Deleting deleted experiences",
            )
            for hash_key in deleted_hashes:
                del data.targets[branch].hashes[hash_key]
                # rm -rf the hash directory on the target host, or the current host
                #  if the host isn't specified
                if controller_host:
                    try:
                        subprocess.run(
                            [
                                "ssh",
                                *shlex.split(ssh_production_option()),
                                controller_host,
                                f"rm -rf {controller_fs_path}/experiences/{hash_key}",
                            ]
                        ).check_returncode()
                    except subprocess.CalledProcessError:
                        set_commit_status(
                            repository_name,
                            sha,
                            "failure",
                            event_name,
                            "Failed to delete removed experiences on remote host",
                        )
                        raise
                else:
                    try:
                        subprocess.run(
                            [
                                "rm",
                                "-rf",
                                f"{controller_fs_path}/experiences/{hash_key}",
                            ]
                        ).check_returncode()
                    except subprocess.CalledProcessError:
                        set_commit_status(
                            repository_name,
                            sha,
                            "failure",
                            event_name,
                            "Failed to delete removed experiences on local host",
                        )
                        raise

            set_commit_status(
                repository_name,
                sha,
                "pending",
                event_name,
                "Syncing experiences",
            )
            for hash_key in changed_hashes | new_hashes:
                # rsync the changed directory to the directory on the target host
                try:
                    subprocess.run(
                        [
                            "rsync",
                            "-avP",
                            "--delete",
                            "-e",
                            rsync_ssh_production_command(),
                            # Hack to avoid dealing with video files for now
                            "--exclude=*.mp4",
                            "--exclude=*.webm",
                            f'{str(temp_path / "experiences" / hash_key)}/',
                            f"{target.controller_path}/experiences/{hash_key}",
                        ]
                    ).check_returncode()
                except subprocess.CalledProcessError:
                    set_commit_status(
                        repository_name,
                        sha,
                        "failure",
                        event_name,
                        f"Failed to sync experience '{hash_key}' to target",
                    )
                    raise

            data.targets[branch].hashes = hashes
            save_build_data(data)

            set_commit_status(
                repository_name, sha, "pending", event_name, "Syncing editorial configs"
            )
            try:
                subprocess.run(
                    [
                        "rsync",
                        "-avP",
                        "--delete",
                        "-e",
                        rsync_ssh_production_command(),
                        *(
                            f"{str(temp_path)}/{file}.toml"
                            for file in ["folders", "tags", "collections"]
                        ),
                        str(target.controller_path),
                    ]
                ).check_returncode()
            except subprocess.CalledProcessError:
                set_commit_status(
                    repository_name,
                    sha,
                    "failure",
                    event_name,
                    "Failed to sync editorial configs",
                )
                raise

            set_commit_status(
                repository_name, sha, "pending", event_name, "Reloading controller"
            )
            try:
                reload_controller(target)
            except requests.RequestException:
                set_commit_status(
                    repository_name,
                    sha,
                    "failure",
                    event_name,
                    "Failed to reload controller",
                )
                raise

    # "Successful in {}s"
    set_commit_status(
        repository_name,
        sha,
        "success",
        event_name,
        f"Successful in {int(round(time.time() - start_time))}s",
    )

def handle_workflow_job_queued(event):
    workflow_job = event["workflow_job"]

    if workflow_job["name"] != "build":
        return

    run_response = github_get_request(workflow_job["run_url"])
    event_name = run_response["name"]
    if (
        run_response["event"] != "push"
        or event_name
        not in [
            "build-experiences",
            "build-controls",
        ]
        or run_response["head_branch"] not in config.targets
    ):
        return

    sha = workflow_job["head_sha"]

    repository_name = event["repository"]["full_name"]
    # Use Python GitHub API to set a status on the commit
    github.get_repo(repository_name).get_commit(sha).create_status(
        "pending",
        context=f"footron-ci/{event_name}",
        description="Waiting for build to finish",
    )

@app.post("/build/webhook")
async def handle_webhook(request: Request):
    # Verify the payload signature X-Hub-Signature
    await verify_github_webhook(request)

    payload = await request.json()
    event_type = request.headers.get("X-GitHub-Event")

    if event_type == "workflow_run" and payload["action"] == "completed":
        handle_workflow_run_completed(payload)
    elif event_type == "workflow_job" and payload["action"] == "queued":
        handle_workflow_job_queued(payload)

    # TODO: Handle adding status for pushing to machine
    # https://docs.github.com/en/rest/commits#commit-statuses

    return {"status": "ok"}