package space.animefu.starfallhaven;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;

/** Bounded, fixed-purpose client for the Starfall Chronicle server proxy. */
public final class ChronicleClient implements AutoCloseable {
    static final String ENDPOINT="https://animefu.space/api/starfall-chronicle";
    static final int MAX_REQUEST_BYTES=4096,MAX_RESPONSE_BYTES=8192;
    private static final int CONNECT_TIMEOUT_MS=5000,READ_TIMEOUT_MS=30000;

    public static final class Request {
        public final String promptVersion,incidentId,headline,synopsis,tone;
        public final int day,settlers,tension;
        public final List<String> permittedFacts;
        public Request(ChronicleDirector.NarrativePrompt prompt){
            if(prompt==null)throw new IllegalArgumentException("narrative prompt is required");
            promptVersion=bounded(prompt.promptVersion,40);incidentId=bounded(prompt.incidentId,48);headline=bounded(prompt.headline,120);synopsis=bounded(prompt.synopsis,240);tone=bounded(prompt.tone,120);
            day=Math.max(1,prompt.day);settlers=clamp(prompt.settlers,0,200);tension=clamp(prompt.tension,0,100);
            List<String> copy=new ArrayList<>();if(prompt.permittedFacts!=null)for(int i=0;i<Math.min(8,prompt.permittedFacts.size());i++)copy.add(bounded(prompt.permittedFacts.get(i),80));permittedFacts=Collections.unmodifiableList(copy);
        }
        public Request(ChronicleHistory.Entry entry,int settlers,int tension){
            if(entry==null)throw new IllegalArgumentException("chronicle entry is required");
            promptVersion="starfall-chronicle-1";incidentId=bounded(entry.incidentId,48);headline=bounded(entry.headline,120);synopsis=bounded(entry.story,240);tone=bounded(entry.tone,120);
            day=Math.max(1,entry.day);this.settlers=clamp(settlers,0,200);this.tension=clamp(tension,0,100);permittedFacts=Collections.emptyList();
        }
    }
    public static final class Narrative {
        public final String title,story,tone;
        Narrative(String title,String story,String tone){this.title=title;this.story=story;this.tone=tone;}
    }
    public interface Callback { void onSuccess(Narrative narrative); void onFailure(String message); }

    private final ExecutorService executor;
    private final AtomicBoolean inFlight=new AtomicBoolean();
    public ChronicleClient(){executor=Executors.newSingleThreadExecutor(new ThreadFactory(){@Override public Thread newThread(Runnable r){Thread t=new Thread(r,"chronicle-network");t.setDaemon(true);return t;}});}
    public boolean narrate(final Request request,final Callback callback){
        if(request==null||callback==null)throw new IllegalArgumentException("request and callback are required");
        if(!inFlight.compareAndSet(false,true))return false;
        executor.execute(()->{try{callback.onSuccess(post(request));}catch(Exception e){callback.onFailure(publicError(e));}finally{inFlight.set(false);}});return true;
    }
    public boolean isBusy(){return inFlight.get();}
    @Override public void close(){executor.shutdownNow();}

    private static Narrative post(Request request)throws IOException{
        byte[] body=encodeRequest(request).getBytes(StandardCharsets.UTF_8);if(body.length>MAX_REQUEST_BYTES)throw new IOException("Chronicle request is too large");
        HttpURLConnection connection=(HttpURLConnection)new URL(ENDPOINT).openConnection();
        try{
            connection.setConnectTimeout(CONNECT_TIMEOUT_MS);connection.setReadTimeout(READ_TIMEOUT_MS);connection.setInstanceFollowRedirects(false);connection.setRequestMethod("POST");connection.setDoOutput(true);connection.setUseCaches(false);
            connection.setRequestProperty("Content-Type","application/json; charset=utf-8");connection.setRequestProperty("Accept","application/json");connection.setFixedLengthStreamingMode(body.length);
            try(OutputStream out=connection.getOutputStream()){out.write(body);}
            int code=connection.getResponseCode();if(code<200||code>=300){drainBounded(connection.getErrorStream(),1024);throw new IOException("Chronicle service returned HTTP "+code);}
            int declared=connection.getContentLength();if(declared>MAX_RESPONSE_BYTES)throw new IOException("Chronicle response is too large");
            String json=new String(readBounded(connection.getInputStream(),MAX_RESPONSE_BYTES),StandardCharsets.UTF_8);
            return parseResponse(json);
        }finally{connection.disconnect();}
    }
    static String encodeRequest(Request r){
        String event=eventEnum(r.incidentId),context=r.synopsis.isEmpty()?r.headline:r.synopsis;
        return new StringBuilder(512).append('{').append("\"event\":\"").append(escape(event))
            .append("\",\"day\":").append(r.day).append(",\"tension\":").append(r.tension)
            .append(",\"colony\":{\"settlers\":").append(r.settlers)
            .append(",\"children\":0,\"wood\":0,\"stone\":0,\"food\":0,\"crystal\":0},\"context\":\"")
            .append(escape(bounded(context,280))).append("\"}").toString();
    }
    private static String eventEnum(String id){return bounded(id,48).toUpperCase(java.util.Locale.US).replace('-','_');}
    static Narrative parseResponse(String json)throws IOException{
        if(json==null||json.length()>MAX_RESPONSE_BYTES)throw new IOException("Invalid Chronicle response");
        String title=jsonString(json,"title"),story=jsonString(json,"story"),tone=jsonString(json,"tone");
        title=bounded(title,120);story=bounded(story,1200);tone=bounded(tone,48);
        if(title.isEmpty()||story.isEmpty()||tone.isEmpty())throw new IOException("Chronicle response is missing required fields");
        return new Narrative(title,story,tone);
    }
    /** Minimal strict-enough parser for three JSON string fields; no Android dependency. */
    private static String jsonString(String json,String key)throws IOException{
        String needle="\""+key+"\"";int at=json.indexOf(needle);if(at<0)throw new IOException("Missing "+key);int colon=json.indexOf(':',at+needle.length());if(colon<0)throw new IOException("Invalid "+key);int i=colon+1;while(i<json.length()&&Character.isWhitespace(json.charAt(i)))i++;if(i>=json.length()||json.charAt(i)!='\"')throw new IOException("Invalid "+key);i++;
        StringBuilder out=new StringBuilder();boolean closed=false;while(i<json.length()){char c=json.charAt(i++);if(c=='\"'){closed=true;break;}if(c=='\\'){if(i>=json.length())throw new IOException("Invalid escape");char e=json.charAt(i++);switch(e){case '\"':case '\\':case '/':out.append(e);break;case 'b':out.append('\b');break;case 'f':out.append('\f');break;case 'n':out.append('\n');break;case 'r':out.append('\r');break;case 't':out.append('\t');break;case 'u':if(i+4>json.length())throw new IOException("Invalid unicode escape");try{out.append((char)Integer.parseInt(json.substring(i,i+4),16));}catch(NumberFormatException ex){throw new IOException("Invalid unicode escape");}i+=4;break;default:throw new IOException("Invalid escape");}}else{if(c<0x20)throw new IOException("Invalid control character");out.append(c);}if(out.length()>1400)throw new IOException("Chronicle field is too large");}if(!closed)throw new IOException("Unterminated "+key);return out.toString().trim();
    }
    private static byte[] readBounded(InputStream in,int max)throws IOException{if(in==null)throw new IOException("Empty Chronicle response");try(InputStream source=in;ByteArrayOutputStream out=new ByteArrayOutputStream(Math.min(max,1024))){byte[] buffer=new byte[1024];int total=0,n;while((n=source.read(buffer))!=-1){total+=n;if(total>max)throw new IOException("Chronicle response is too large");out.write(buffer,0,n);}return out.toByteArray();}}
    private static void drainBounded(InputStream in,int max){if(in==null)return;try{readBounded(in,max);}catch(IOException ignored){}}
    private static String publicError(Exception e){String m=e.getMessage();if(m==null||m.isEmpty())return "Chronicle service unavailable";return bounded(m,120);}
    private static int clamp(int value,int min,int max){return Math.max(min,Math.min(max,value));}
    private static String bounded(String value,int max){if(value==null)return "";String clean=value.replace('\u0000',' ').trim();return clean.length()<=max?clean:clean.substring(0,max).trim();}
    private static String escape(String value){StringBuilder out=new StringBuilder(value.length()+16);for(int i=0;i<value.length();i++){char c=value.charAt(i);switch(c){case '\"':out.append("\\\"");break;case '\\':out.append("\\\\");break;case '\b':out.append("\\b");break;case '\f':out.append("\\f");break;case '\n':out.append("\\n");break;case '\r':out.append("\\r");break;case '\t':out.append("\\t");break;default:if(c<0x20)out.append(String.format("\\u%04x",(int)c));else out.append(c);}}return out.toString();}
}
