
from datetime import date
from ._imports import *


class HailData:

    '''
    hd = HailData()
    data = hd.get_hail_data()
    df, gdf = hd.extract_data_to_df(data)
    hd.save_data_csv(df, gdf, path= r'D:\Cao_A036898\project\hail\gis_file' )
    print(f"hail_swath_{hd.file_timestamp}.gpkg")
    gdf.head()
    
    '''

    TASK_URL = "https://gisdev.amica.com/arcgis/rest/services/GPTools/GetHailMap/GPServer/GetHailMap/"  #submitJob

    DATE_PARAM_NAME = "hailDate"

    def __init__ (self, cat_date = '2015-04-19'):
        self.cat_date = cat_date
        self.file_timestamp = dt.datetime.now().strftime('%Y%m%d_%H%M%S')


    def submit_gp_job(self, task_url, date_param_name, submit_date):
        """Submits an asynchronous geoprocessing job."""
        submit_url = f"{task_url}/submitJob"
        
        # Parameters for the REST request, including the date
        params = {
            date_param_name: submit_date,
            'f': 'json' # Requesting a JSON response
        }
        
        # If using a token, add it to params:
        # params['token'] = TOKEN

        print(f"Submitting job to {submit_url} with date {submit_date}...")
        response = requests.post(submit_url, data=params)
        response.raise_for_status()
        job_info = response.json()
        
        if 'jobId' in job_info:
            job_id = job_info['jobId']
            print(f"Job submitted with ID: {job_id}")
            return job_id
        else:
            raise Exception(f"Failed to submit job: {job_info.get('messages')}")

    def monitor_job_status(self, task_url, job_id):
        """Monitors the job status periodically until completion."""
        job_url = f"{task_url}/jobs/{job_id}"
        
        # If using a token, add it to the URL query parameters
        # job_url = f"{job_url}?token={TOKEN}" 

        status = ""
        while status not in ["esriJobSucceeded", "esriJobFailed", "esriJobCancelled"]:
            print(f"Checking job status (current: {status})...")
            time.sleep(2) # Wait a bit before checking again to avoid overwhelming the server
            
            response = requests.get(job_url, params={'f': 'json'})
            response.raise_for_status()
            job_response = response.json()
            status = job_response.get('jobStatus')
            
            if status == "esriJobFailed":
                print("Job failed. Messages:")
                for msg in job_response.get('messages', []):
                    print(f"- {msg['description']}")
                raise Exception("Geoprocessing job failed.")
                
        print(f"Job finished with status: {status}")
        return job_response

    def get_job_results(self, job_url, job_response):
        """Retrieves the URLs to the job results."""
        results_urls = {}
        if 'results' in job_response:
            for param_name, param_info in job_response['results'].items():
                result_url = f"{job_url}/{param_info['paramUrl']}"
                
                # If using a token, append it to the result URL
                # result_url = f"{result_url}?token={TOKEN}" 
                
                results_urls[param_name] = result_url
        return results_urls

    def get_hail_data(self):
        try:
            # 1. Submit the job
            job_id = self.submit_gp_job(self.TASK_URL, self.DATE_PARAM_NAME, self.cat_date)
            job_url = f"{HailData.TASK_URL}/jobs/{job_id}"

            # 2. Monitor status until finished
            final_job_info = self.monitor_job_status(HailData.TASK_URL, job_id)

            # 3. Get results URLs
            result_urls = self.get_job_results(job_url, final_job_info)
            # print("\nJob results URLs:")
            # for name, url in result_urls.items():
            #     print(f"- {name}: {url}")

            # 4. get async results 
            response = requests.get(url=result_urls['hailSwath'] + "?f=pjson")
            data = response.json()
            return data

        except Exception as e:
            print(f"\nAn error occurred: {e}")

    
    def extract_data_to_df (self, data):        
        features = data['value']['features']
        attrs_rows = []
        geoms = []
        for f in features:
            attrs = (f.get("attributes") or {}).copy()
            geom = f.get("geometry") or {}
            shp = esri_polygon_to_shapely(geom)
            if shp is not None:
                attrs_rows.append(attrs)
                geoms.append(shp)
        df = pd.DataFrame(attrs_rows)
        gdf = gpd.GeoDataFrame(df, geometry=geoms, crs=f"EPSG:{4326}")  #by default use 4326
        return df, gdf
    
    def save_data_csv(self,df, gdf, path= None):

        file_path =  path if path is not None else ''  

        # Geopackage (single-file, recommended)        
        gdf.to_file(os.path.join(file_path, f"hail_swath_{self.file_timestamp}.gpkg"), layer="hail_swath", driver="GPKG")

        # CSV with WKT for systems that aren't GIS-aware
        df_out = df.copy()
        df_out["WKT"] = gdf.geometry.to_wkt()
        df_out.to_csv(os.path.join(file_path, f"hail_swath_{self.file_timestamp}.csv"), index=False)
        return 


# ------------------------------
# 4) ArcGIS rings → Shapely geometry → GeoDataFrame
# ------------------------------
def ring_area(coords):
    # Shoelace formula (signed area); coords are closed rings [ (x,y), ..., (x,y) ]
    a = 0.0
    for i in range(len(coords) - 1):
        x1, y1 = coords[i]
        x2, y2 = coords[i+1]
        a += x1*y2 - x2*y1
    return a / 2.0

def esri_polygon_to_shapely(geom_esri):
    """
    Convert ArcGIS polygon JSON (with 'rings') to Shapely Polygon/MultiPolygon.
    Positive signed area -> outer ring; negative -> hole for the most recent outer.
    """
    rings = geom_esri.get("rings", [])
    if not rings:
        return None

    outers, holes_list = [], []
    for ring in rings:
        if ring[0] != ring[-1]:
            ring = ring + [ring[0]]
        if ring_area(ring) > 0:
            # new outer shell
            outers.append(ring)
            holes_list.append([])
        else:
            # hole goes to the most recent outer
            if holes_list:
                holes_list[-1].append(ring)
            else:
                # fallback: treat as standalone outer if no outer has been seen yet
                outers.append(ring)
                holes_list.append([])

    if len(outers) == 1:
        return Polygon(outers[0], holes=holes_list[0] if holes_list else None)

    # multiple outers → MultiPolygon
    polys = []
    for i, outer in enumerate(outers):
        holes = holes_list[i] if i < len(holes_list) else None
        polys.append(Polygon(outer, holes=holes))
    return MultiPolygon(polys)


    