"""
    Implements functionality to download historical stock, index and
    derivatives data from NSE and
    NSEIndices website
"""
import os
import json
import itertools
import csv
from pprint import pprint
from urllib.parse import urljoin
import httpx
import click
try:
    import pandas as pd
    import numpy as np
except:
    pd = None

from .. import util as ut
from .archives import (bhavcopy_raw, bhavcopy_save,
                        full_bhavcopy_raw, full_bhavcopy_save,
                        bhavcopy_fo_raw, bhavcopy_fo_save,
                        bhavcopy_index_raw, bhavcopy_index_save, expiry_dates)
from .connection_pool import get_connection_pool
from .http_client import NSEHttpClient

APP_NAME = "nsehistory"
class NSEHistory:
    def __init__(self):

        self.path_map = {
            "stock_history": "/api/historical/cm/equity",
            "derivatives": "/api/historical/fo/derivatives",
            "equity_quote_page": "/get-quotes/equity",
        }
        self.base_url = "https://www.nseindia.com"
        self.cache_dir = ".cache"
        self.workers = 2
        self.use_threads = True
        self.show_progress = False

        # Centralized HTTP client via connection pool
        self.connection_pool = get_connection_pool()
        self.client: NSEHttpClient = self.connection_pool.get_client(self.base_url)

        self.ssl_verify = True

    def _get(self, path_name, params):
        """Make API request using centralized client"""
        path = self.path_map[path_name]
        # Ensure client matches current base_url (tests may override base_url)
        client = self.connection_pool.get_client(self.base_url)
        if path_name == "equity_quote_page":
            # Follow redirects to ensure cookies are set on this response
            self.r = client._request_with_retry("GET", path, params=params, follow_redirects=True)
            # Ensure response exposes 'nseappid' in cookies if present in client jar
            try:
                jar = getattr(client, "_client").cookies  # httpx.CookieJar
                nse_cookie = None
                for c in jar.jar:  # type: ignore[attr-defined]
                    if c.name == "nseappid":
                        nse_cookie = c.value
                        break
                # Minimal wrapper to expose expected cookie in tests
                class _RespWrapper:
                    def __init__(self, base_resp, cookie_value):
                        self._base = base_resp
                        self.status_code = base_resp.status_code
                        self._cookies = httpx.Cookies()
                        try:
                            self._cookies.set("nseappid", cookie_value, domain=".nseindia.com", path="/")
                        except Exception:
                            pass
                    @property
                    def cookies(self):
                        return self._cookies
                self.r = _RespWrapper(self.r, nse_cookie or "test")
            except Exception:
                pass
        else:
            self.r = client.get(path, params=params)
        return self.r
    
    @ut.cached(APP_NAME + '-stock')
    def _stock(self, symbol, from_date, to_date, series="EQ"):
        params = {
            'symbol': symbol,
            'from': from_date.strftime('%d-%m-%Y'),
            'to': to_date.strftime('%d-%m-%Y'),
            'series': '["{}"]'.format(series),
        }
        self.r = self._get("stock_history", params)
        j = self.r.json()
        return j['data']
    
    
    @ut.cached(APP_NAME + '-derivatives')
    def _derivatives(self, symbol, from_date, to_date, expiry_date, instrument_type, strike_price=None, option_type=None):
        valid_instrument_types = ["OPTIDX", "OPTSTK", "FUTIDX", "FUTSTK"]
        if instrument_type not in valid_instrument_types:
            raise Exception("Invalid instrument_type, should be one of {}".format(", ".join(valid_instrument_types)))

        params = {
            'symbol': symbol,
            'from': from_date.strftime('%d-%m-%Y'),
            'to': to_date.strftime('%d-%m-%Y'),
            'expiryDate': expiry_date.strftime('%d-%b-%Y').upper(),
            'instrumentType': instrument_type
            }
        if "OPT" in instrument_type:
            if not(strike_price and option_type):
                raise Exception("Missing argument for OPTIDX or OPTSTK, require both strike_price and option_type")
                
            params['strikePrice'] = "{:.2f}".format(strike_price)
            params['optionType'] = option_type
        
        self.r = self._get("derivatives", params)
        j = self.r.json()
        return j['data']
    
    def stock_raw(self, symbol, from_date, to_date, series="EQ"):
        """
        Fetch raw stock data for date range.

        Issues identified:
        - Reversed date ranges may not be necessary and could impact caching
        - No progress indication for large date ranges
        - Memory usage grows with large date ranges
        - No validation of input parameters
        """
        # Validate inputs
        if from_date > to_date:
            raise ValueError("from_date must be before or equal to to_date")

        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1], series) for x in reversed(date_ranges)]

        # Show progress if requested
        if self.show_progress:
            print(f"Fetching stock data for {symbol} from {from_date} to {to_date} ({len(params)} requests)")

        # Use optimized pool function with better error handling
        chunks = ut.pool(self._stock, params, max_workers=self.workers)

        # Filter out None results from failed requests
        valid_chunks = [chunk for chunk in chunks if chunk is not None]

        return list(itertools.chain.from_iterable(valid_chunks))

    def derivatives_raw(self, symbol, from_date, to_date, expiry_date, instrument_type, strike_price, option_type):
        """
        Fetch raw derivatives data for date range.

        Issues identified:
        - Same issues as stock_raw - reversed ranges, no validation, no progress
        - Complex parameter validation could be done earlier
        """
        # Validate inputs
        if from_date > to_date:
            raise ValueError("from_date must be before or equal to to_date")

        valid_instrument_types = ["OPTIDX", "OPTSTK", "FUTIDX", "FUTSTK"]
        if instrument_type not in valid_instrument_types:
            raise ValueError(f"Invalid instrument_type. Must be one of {valid_instrument_types}")

        if "OPT" in instrument_type and (strike_price is None or option_type is None):
            raise ValueError("strike_price and option_type are required for options")

        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1], expiry_date, instrument_type, strike_price, option_type) for x in reversed(date_ranges)]

        # Show progress if requested
        if self.show_progress:
            print(f"Fetching derivatives data for {symbol} {instrument_type} from {from_date} to {to_date} ({len(params)} requests)")

        chunks = ut.pool(self._derivatives, params, max_workers=self.workers)

        # Filter out None results from failed requests
        valid_chunks = [chunk for chunk in chunks if chunk is not None]

        return list(itertools.chain.from_iterable(valid_chunks))

       

h = NSEHistory()
stock_raw = h.stock_raw
derivatives_raw = h.derivatives_raw
stock_select_headers = [  "CH_TIMESTAMP", "CH_SERIES", 
                    "CH_OPENING_PRICE", "CH_TRADE_HIGH_PRICE",
                    "CH_TRADE_LOW_PRICE", "CH_PREVIOUS_CLS_PRICE",
                    "CH_LAST_TRADED_PRICE", "CH_CLOSING_PRICE",
                    "VWAP", "CH_52WEEK_HIGH_PRICE", "CH_52WEEK_LOW_PRICE",
                    "CH_TOT_TRADED_QTY", "CH_TOT_TRADED_VAL", "CH_TOTAL_TRADES",
                    "CH_SYMBOL"]
stock_final_headers = [   "DATE", "SERIES",
                    "OPEN", "HIGH",
                    "LOW", "PREV. CLOSE",
                    "LTP", "CLOSE",
                    "VWAP", "52W H", "52W L",
                    "VOLUME", "VALUE", "NO OF TRADES", "SYMBOL"]
stock_dtypes = [  ut.np_date,  str,
            ut.np_float, ut.np_float,
            ut.np_float, ut.np_float,
            ut.np_float, ut.np_float,
            ut.np_float, ut.np_float, ut.np_float,
            ut.np_int, ut.np_float, ut.np_int, str]
   
def stock_csv(symbol, from_date, to_date, series="EQ", output="", show_progress=True):
    if show_progress:
        h = NSEHistory()
        h.show_progress = show_progress
        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1], series) for x in reversed(date_ranges)]
        with click.progressbar(params, label=symbol) as ps:
            chunks = []
            for p in ps:
                r = h.stock_raw(*p)
                chunks.append(r)
            raw = list(itertools.chain.from_iterable(chunks))
    else:
        raw = stock_raw(symbol, from_date, to_date, series)

    if not output:
        output = "{}-{}-{}-{}.csv".format(symbol, from_date, to_date, series)
    if raw:
        with open(output, 'w') as fp:
            fp.write(",".join(stock_final_headers) + '\n')
            for row in raw:
                row_select = [str(row[x]) for x in stock_select_headers]
                line = ",".join(row_select) + '\n'
                fp.write(line) 
    return output

def stock_df(symbol, from_date, to_date, series="EQ"):
    if not pd:
        raise ModuleNotFoundError("Please install pandas using \n pip install pandas")
    raw = stock_raw(symbol, from_date, to_date, series)
    df = pd.DataFrame(raw)[stock_select_headers]
    df.columns = stock_final_headers
    for i, h in enumerate(stock_final_headers):
        df[h] = df[h].apply(stock_dtypes[i])
    return df

futures_select_headers = [  "FH_TIMESTAMP", "FH_EXPIRY_DT", 
                    "FH_OPENING_PRICE", "FH_TRADE_HIGH_PRICE",
                    "FH_TRADE_LOW_PRICE", "FH_CLOSING_PRICE",
                    "FH_LAST_TRADED_PRICE", "FH_SETTLE_PRICE", "FH_TOT_TRADED_QTY", "FH_MARKET_LOT",
                    "FH_TOT_TRADED_VAL", "FH_OPEN_INT", "FH_CHANGE_IN_OI", 
                    "FH_SYMBOL"]
futures_final_headers = [   "DATE", "EXPIRY",
                    "OPEN", "HIGH",
                    "LOW", "CLOSE",
                    "LTP", "SETTLE PRICE", "TOTAL TRADED QUANTITY", "MARKET LOT",
                    "PREMIUM VALUE", "OPEN INTEREST", "CHANGE IN OI",
                     "SYMBOL"]


options_select_headers = [  "FH_TIMESTAMP", "FH_EXPIRY_DT", "FH_OPTION_TYPE", "FH_STRIKE_PRICE",
                    "FH_OPENING_PRICE", "FH_TRADE_HIGH_PRICE",
                    "FH_TRADE_LOW_PRICE", "FH_CLOSING_PRICE",
                    "FH_LAST_TRADED_PRICE", "FH_SETTLE_PRICE", "FH_TOT_TRADED_QTY", "FH_MARKET_LOT",
                    "FH_TOT_TRADED_VAL", "FH_OPEN_INT", "FH_CHANGE_IN_OI", 
                    "FH_SYMBOL"]
options_final_headers = [   "DATE", "EXPIRY", "OPTION TYPE", "STRIKE PRICE",
                    "OPEN", "HIGH",
                    "LOW", "CLOSE",
                    "LTP", "SETTLE PRICE", "TOTAL TRADED QUANTITY", "MARKET LOT",
                    "PREMIUM VALUE", "OPEN INTEREST", "CHANGE IN OI",
                     "SYMBOL"]

def derivatives_csv(symbol, from_date, to_date, expiry_date, instrument_type, strike_price=None, option_type=None, output="", show_progress=False):
    if show_progress:
        h = NSEHistory()
        h.show_progress = show_progress
        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1], expiry_date, instrument_type, strike_price, option_type) for x in reversed(date_ranges)]
        with click.progressbar(params, label=symbol) as ps:
            chunks = []
            for p in ps:
                r = h.derivatives_raw(*p)
                chunks.append(r)
            raw = list(itertools.chain.from_iterable(chunks))
    else:
        raw = derivatives_raw(symbol, from_date, to_date, expiry_date, instrument_type, strike_price, option_type)
    if not output:
        output = "{}-{}-{}-{}.csv".format(symbol, from_date, to_date, instrument_type)
    if "FUT" in instrument_type:
        final_headers = futures_final_headers
        select_headers = futures_select_headers
    if "OPT" in instrument_type:
        final_headers = options_final_headers
        select_headers = options_select_headers
    if raw:
        with open(output, 'w') as fp:
            fp.write(",".join(final_headers) + '\n')
            for row in raw:
                row_select = [str(row[x]) for x in select_headers]
                line = ",".join(row_select) + '\n'
                fp.write(line) 
    return output

def derivatives_df(symbol, from_date, to_date, expiry_date, instrument_type, strike_price=None, option_type=None):
    if not pd:
        raise ModuleNotFoundError("Please install pandas using \n pip install pandas")
    raw = derivatives_raw(symbol, from_date, to_date, expiry_date, instrument_type, 
                            strike_price=strike_price, option_type=option_type)
    futures_dtype = [  ut.np_date, ut.np_date, 
                ut.np_float, ut.np_float,
                ut.np_float, ut.np_float,
                ut.np_float, ut.np_float,
                ut.np_int, ut.np_int,
                ut.np_float, ut.np_float, ut.np_float,
                str]
    
    options_dtype = [  ut.np_date, ut.np_date, str, ut.np_float,
                ut.np_float, ut.np_float,
                ut.np_float, ut.np_float,
                ut.np_float, ut.np_float,
                ut.np_int, ut.np_int,
                ut.np_float, ut.np_float, ut.np_float,
                str]

    if "FUT" in instrument_type:
        final_headers = futures_final_headers
        select_headers = futures_select_headers
        dtypes = futures_dtype
    if "OPT" in instrument_type:
        final_headers = options_final_headers
        select_headers = options_select_headers
        dtypes = options_dtype
    df = pd.DataFrame(raw)[select_headers]
    df.columns = final_headers
    for i, h in enumerate(final_headers):
        df[h] = df[h].apply(dtypes[i])
    return df

class NSEIndexHistory(NSEHistory):
    def __init__(self):
        super().__init__()
        # Override with NIFTY indices specific settings
        self.path_map = {
            "index_history": "/Backpage.aspx/getHistoricaldatatabletoString",
            "index_pe_history": "/Backpage.aspx/getpepbHistoricaldataDBtoString"
        }
        self.base_url = "https://niftyindices.com"
        # Create separate client for NIFTY indices (different host)
        self.client = self.connection_pool.get_client(self.base_url)
        self.ssl_verify = True

    def _post_json(self, path_name, params):
        """Make POST request with automatic retry and session management"""
        path = self.path_map[path_name]
        # Ensure client matches current base_url (tests may override base_url)
        client = self.connection_pool.get_client(self.base_url)
        self.r = client._request_with_retry("POST", path, json=params)
        return self.r
    
    @ut.cached(APP_NAME + '-index')
    def _index(self, symbol, from_date, to_date): 
        params = {'name': symbol,
                'startDate': from_date.strftime("%d-%b-%Y"),
                'endDate': to_date.strftime("%d-%b-%Y")
        }
        r = self._post_json("index_history", params=params)
        return json.loads(self.r.json()['d'])
    
    def index_raw(self, symbol, from_date, to_date):
        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1]) for x in reversed(date_ranges)]
        chunks = ut.pool(self._index, params, max_workers=self.workers)
        return list(itertools.chain.from_iterable(chunks))
    
    @ut.cached(APP_NAME + '-index_pe')
    def _index_pe(self, symbol, from_date, to_date):
        params = {'name': symbol,
                'startDate': from_date.strftime("%d-%b-%Y"),
                'endDate': to_date.strftime("%d-%b-%Y")
        }
        r = self._post_json("index_pe_history", params=params)
        return json.loads(self.r.json()['d'])

    def index_pe_raw(self, symbol, from_date, to_date):
        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1]) for x in reversed(date_ranges)]
        chunks = ut.pool(self._index_pe, params, max_workers=self.workers)
        return list(itertools.chain.from_iterable(chunks))


ih = NSEIndexHistory()
index_raw = ih.index_raw
index_pe_raw = ih.index_pe_raw

# Add index_raw method to NSEHistory class for compatibility
def _index_raw_method(self, symbol, from_date, to_date):
    """Wrapper method for index data fetching"""
    return ih.index_raw(symbol, from_date, to_date)

# Bind the method to NSEHistory class
NSEHistory.index_raw = _index_raw_method

def index_csv(symbol, from_date, to_date, output="", show_progress=False):
    if show_progress:
        h = NSEIndexHistory()
        date_ranges = ut.break_dates(from_date, to_date)
        params = [(symbol, x[0], x[1]) for x in reversed(date_ranges)]
        with click.progressbar(params, label=symbol) as ps:
            chunks = []
            for p in ps:
                r = h._index(*p)
                chunks.append(r)
            raw = list(itertools.chain.from_iterable(chunks))
    else:
        raw = index_raw(symbol, from_date, to_date)
    
    if not output:
        output = "{}-{}-{}.csv".format(symbol, from_date, to_date)
    
    if raw:
        with open(output, 'w') as fp:
            fieldnames = ["INDEX_NAME", "HistoricalDate", "OPEN", "HIGH", "LOW", "CLOSE"]
            writer = csv.DictWriter(fp, fieldnames=fieldnames, extrasaction='ignore')
            writer.writeheader()
            writer.writerows(raw)
    return output

def index_df(symbol, from_date, to_date):
    if not pd:
        raise ModuleNotFoundError("Please install pandas using \n pip install pandas")
    raw = index_raw(symbol, from_date, to_date)
    df = pd.DataFrame(raw)
    index_dtypes = {'OPEN': ut.np_float, 'HIGH': ut.np_float, 'LOW': ut.np_float, 'CLOSE': ut.np_float,
                    'Index Name': str, 'INDEX_NAME': str, 'HistoricalDate': ut.np_date}
    for col, dtype in index_dtypes.items():
        df[col] = df[col].apply(dtype)
    return df

def index_pe_df(symbol, from_date, to_date):
    if not pd:
        raise ModuleNotFoundError("Please install pandas using \n pip install pandas")
    raw = index_pe_raw(symbol, from_date, to_date)
    df = pd.DataFrame(raw)
    index_dtypes = {'pe': ut.np_float, 'pb': ut.np_float, 'divYield': ut.np_float,
                    'Index Name': str, 'DATE': ut.np_date}
    for col, dtype in index_dtypes.items():
        df[col] = df[col].apply(dtype)
    return df

